blob: 15aee31796a28b0a38201c43e6237e1c6950d108 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
var _ = require('lodash');
var debug = require('debug')('background:index');
var domready = require('domready');
var AuthHandler = require('./auth-handler');
var extensionErrors = require('../../../src/browser/extension-errors');
var getOrigin = require('./util').getOrigin;
var Nacl = require('./nacl');
domready(function() {
// Start!
var bp = new BackgroundPage();
chrome.runtime.onConnect.addListener(
bp.handleNewContentScriptConnection.bind(bp));
// Set bp on the window so it will be accessable from options page.
window.bp = bp;
// Expose the state object so options page and background can share it and
// stay in sync.
bp.state = require('../state');
});
function BackgroundPage() {
if (!(this instanceof BackgroundPage)) {
return new BackgroundPage();
}
// Map that stores instanceId -> port so messages can be routed back to the
// port they came from.
this.ports = {};
// Map that stores port -> instanceId list so browspr can cleanup the
// instances corresponding when the port when the port closes.
this.instanceIds = new Map();
debug('background script loaded');
}
// Start listening to messages from the Nacl plugin.
BackgroundPage.prototype.registerNaclListeners = function() {
this.nacl.on('message', this.handleMessageFromNacl.bind(this));
this.nacl.on('crash', this.handleNaclCrash.bind(this));
};
// Handle messages coming from Nacl by send them to the associated port.
BackgroundPage.prototype.handleMessageFromNacl = function(msg) {
var instanceId = msg.instanceId;
var port = this.ports[instanceId];
if (!port) {
console.error('Message received not matching instance id: ', instanceId);
return;
}
port.postMessage(msg);
};
// Handle messages coming from a content script.
BackgroundPage.prototype.handleMessageFromContentScript = function(port, msg) {
var bp = this;
if (!this.naclPluginIsActive()) {
// Start the plugin if it is not started.
this.startNaclPlugin(handleMessage.bind(bp, port, msg));
} else {
handleMessage(port, msg);
}
function handleMessage(port, msg) {
// Wrap in process.nextTick so chrome stack traces can use sourceMap.
process.nextTick(function() {
debug('background received message from content script.', msg);
// Dispatch on the type of the message.
switch (msg.type) {
// From vanadium app.
case 'browsprMsg':
return bp.handleBrowsprMessage(port, msg);
case 'browsprCleanup':
return bp.handleBrowsprCleanup(port, msg);
case 'createInstance':
return bp.handleCreateInstance(port, msg);
case 'auth':
return bp.authHandler.handleAuthMessage(port);
// From bless.
case 'assocAccount:finish':
return bp.authHandler.handleFinishAuth(port, msg);
// ONLY for tests.
case 'intentionallyPanic':
return bp._triggerIntentionalPanic();
default:
console.error('unknown message.', msg);
}
});
}
};
// Trigger a panic in the plug-in (only for tests).
BackgroundPage.prototype._triggerIntentionalPanic = function() {
if (process.env.ALLOW_INTENTIONAL_CRASH) {
var panicMsg = {
type: 'intentionallyPanic',
instanceId: 0,
origin: '',
body: ''
};
this.nacl.sendMessage(panicMsg);
}
};
// Handle a content script connecting to this background script.
BackgroundPage.prototype.handleNewContentScriptConnection = function(port) {
port.onMessage.addListener(
this.handleMessageFromContentScript.bind(this, port));
var bp = this;
port.onDisconnect.addListener(function() {
var instanceIds = bp.instanceIds.get(port) || [];
instanceIds.forEach(function(instanceId) {
bp.handleBrowsprCleanup(port, {body: {instanceId: instanceId}});
});
});
};
// Clean up an instance, and tell Nacl to clean it up as well.
BackgroundPage.prototype.handleBrowsprCleanup = function(port, msg) {
function sendCleanupFinishedMessage() {
safePostMessage(port, {type: 'browsprCleanupFinished'});
}
if (!this.naclPluginIsActive()) {
// If the plugin isn't started, no need to clean it up.
sendCleanupFinishedMessage();
return;
}
var instanceId = msg.body.instanceId;
if (!this.ports[instanceId]) {
return console.error('Got cleanup message from instance ' + instanceId +
' with no associated port.');
}
if (this.ports[instanceId] !== port) {
return console.error('Got cleanup message for instance ' + instanceId +
' that does not match port.');
}
var bp = this;
var now = Date.now();
this.nacl.cleanupInstance(instanceId, function() {
var end = Date.now();
console.log('Cleaned up instance: ' + instanceId + ' in ' +
(end - now) + ' ms');
bp.instanceIds.set(port, _.remove(bp.instanceIds.get(port), [instanceId]));
if (bp.instanceIds.get(port).length === 0) {
bp.instanceIds.delete(port);
}
delete bp.ports[instanceId];
sendCleanupFinishedMessage();
bp.stopNaclPluginIfUnused();
});
};
BackgroundPage.prototype.isValidMessageForPort = function(msg, port) {
var body = msg.body;
if (!body) {
console.error('Got message with no body: ', msg);
return false;
}
if (!body.instanceId) {
console.error('Got message with no instanceId: ', msg);
return false;
}
if (this.ports[body.instanceId] && this.ports[body.instanceId] !== port) {
console.error('Got browspr message with instanceId ' +
body.instanceId + ' that does not match port.');
return false;
}
return true;
};
BackgroundPage.prototype.assocPortAndInstanceId = function(port, instanceId) {
// Store the instanceId->port.
this.ports[instanceId] = port;
// Store the port->instanceId.
this.instanceIds.set(port,
_.union(this.instanceIds.get(port) || [], [instanceId]));
// Cache the origin on the port object.
port.origin = port.origin || getOrigin(port.sender.url);
};
// Handle an createInstance message.
BackgroundPage.prototype.handleCreateInstance = function(port, msg) {
if (!this.isValidMessageForPort(msg, port)) {
return console.error('Invalid port for message. Ignoring.');
}
var body = msg.body;
this.assocPortAndInstanceId(port, body.instanceId);
this.nacl.channel.performRpc('create-instance', {
instanceId: body.instanceId,
origin: port.origin,
namespaceRoots: body.settings.namespaceRoots,
proxy: body.settings.proxy
}, function(err) {
if (err) {
return safePostMessage(port, {type: 'createInstance:error', error: err});
}
safePostMessage(port, {type: 'createInstance:success'});
});
};
// Handle messages that will be sent to Nacl.
BackgroundPage.prototype.handleBrowsprMessage = function(port, msg) {
if (!this.isValidMessageForPort(msg, port)) {
return;
}
var body = msg.body;
this.assocPortAndInstanceId(port, body.instanceId);
var naclMsg = {
type: msg.type,
instanceId: parseInt(body.instanceId),
origin: port.origin,
body: body.msg
};
return this.nacl.sendMessage(naclMsg);
};
// Return true if the nacl plug-in is running.
BackgroundPage.prototype.naclPluginIsActive = function() {
return this.hasOwnProperty('nacl') && this.nacl.isReady;
};
// Start the nacl plug-in -- add it to the page and register handlers.
BackgroundPage.prototype.startNaclPlugin = function(cb) {
var bp = this;
cb = cb || function() {};
if (!bp.nacl) {
bp.nacl = new Nacl();
bp.registerNaclListeners();
bp.nacl.once('ready', function() {
bp.authHandler = new AuthHandler(bp.nacl.channel);
});
}
bp.nacl.once('ready', cb.bind(bp));
};
// Stop the nacl plugin if it is not currently used.
BackgroundPage.prototype.stopNaclPluginIfUnused = function() {
if (Object.keys(this.ports).length === 0) {
this.stopNaclPlugin();
}
};
// Stop the nacl plug-in - remove it from the page and clean up state.
BackgroundPage.prototype.stopNaclPlugin = function() {
this.nacl.destroy();
delete this.nacl;
};
// Stop and start the nacl plug-in
BackgroundPage.prototype.restartNaclPlugin = function(cb) {
cb = cb || function() {};
if (this.naclPluginIsActive()) {
this.stopNaclPlugin();
}
this.startNaclPlugin(cb);
};
// Returns an array of all active port objects.
BackgroundPage.prototype.getAllPorts = function() {
var ports = [];
_.forEach(this.ports, function(portArray) {
ports = ports.concat(portArray);
});
// Sort the ports array so that _.uniq can use a faster search algorithm.
ports = _.sortBy(ports);
// The second argument to _.uniq is whether the array is sorted.
return _.uniq(ports, true);
};
// Restart nacl when it crashes.
BackgroundPage.prototype.handleNaclCrash = function(msg) {
// Log the crash to the extension's console.
console.error('NACL plugin crashed.');
if (msg) {
console.error(msg);
}
// Restart the plugin
this.stopNaclPlugin();
// Notify all content scripts about the failure.
var crashNotificationMsg = {
type: 'crash',
body: new extensionErrors.ExtensionCrashError(msg)
};
this.getAllPorts().forEach(function(port) {
safePostMessage(port, crashNotificationMsg);
});
};
function safePostMessage(port, msg) {
try {
port.postMessage(msg);
} catch (e) {
// Port no longer exists. Safe to ignore.
}
}