| // 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. |
| } |
| } |