harness-options.json 0000644 0000000 0000000 00000014120 12464015254 013604 0 ustar root root {
"abort_on_missing": false,
"check_memory": false,
"enable_e10s": false,
"is-sdk-bundled": false,
"jetpackID": "jid1-F9UJ2thwoAm5gQ@jetpack",
"loader": "addon-sdk/lib/sdk/loader/cuddlefish.js",
"main": "main",
"mainPath": "lightbeam/main",
"manifest": {
"lightbeam/connection": {
"docsSHA256": null,
"jsSHA256": "6e04c7d0f55418e3ac5515f5a078bf0087acd843e7028835025cf6607aca0376",
"moduleName": "connection",
"packageName": "lightbeam",
"requirements": {
"./persist": "lightbeam/persist",
"./tab/utils": "lightbeam/tab/utils",
"chrome": "chrome",
"sdk/event/core": "sdk/event/core",
"sdk/self": "sdk/self",
"sdk/simple-storage": "sdk/simple-storage",
"sdk/timers": "sdk/timers"
},
"sectionName": "lib"
},
"lightbeam/main": {
"docsSHA256": "e60ed986f0f22ee750f6b64bac6bb5370aef7d7ba0ab484ce41e3406e8fd04ab",
"jsSHA256": "399fe28a54e3eecc23e7e2b86009ecc497b4613d967fb889bb3247d4c2d3de45",
"moduleName": "main",
"packageName": "lightbeam",
"requirements": {
"./connection": "lightbeam/connection",
"./ui": "lightbeam/ui",
"sdk/page-mod": "sdk/page-mod",
"sdk/self": "sdk/self",
"sdk/system/events": "sdk/system/events",
"sdk/tabs": "sdk/tabs"
},
"sectionName": "lib"
},
"lightbeam/persist": {
"docsSHA256": null,
"jsSHA256": "46d6801e944e9eaf2dbaa516d088a92a186987da3e54bc88ab068d30645bf231",
"moduleName": "persist",
"packageName": "lightbeam",
"requirements": {
"sdk/request": "sdk/request",
"sdk/simple-prefs": "sdk/simple-prefs",
"sdk/simple-storage": "sdk/simple-storage"
},
"sectionName": "lib"
},
"lightbeam/shared/menuitems": {
"docsSHA256": null,
"jsSHA256": "fd132327851fe2c9f6f0990899fdc2cc3f8e124e5a8815d012f56d8d35070b4d",
"moduleName": "shared/menuitems",
"packageName": "lightbeam",
"requirements": {
"./unload+": "lightbeam/shared/unload+",
"sdk/core/heritage": "sdk/core/heritage",
"sdk/core/namespace": "sdk/core/namespace",
"sdk/deprecated/api-utils": "sdk/deprecated/api-utils",
"sdk/deprecated/window-utils": "sdk/deprecated/window-utils",
"sdk/event/core": "sdk/event/core",
"sdk/event/target": "sdk/event/target",
"sdk/window/utils": "sdk/window/utils"
},
"sectionName": "lib"
},
"lightbeam/shared/policy": {
"docsSHA256": null,
"jsSHA256": "0ffd8914168d64cc4f6ce2779e671069e34d0fc5ae4d82d2206d3fb9b52088d0",
"moduleName": "shared/policy",
"packageName": "lightbeam",
"requirements": {
"chrome": "chrome",
"sdk/core/heritage": "sdk/core/heritage",
"sdk/core/namespace": "sdk/core/namespace",
"sdk/deprecated/api-utils": "sdk/deprecated/api-utils",
"sdk/platform/xpcom": "sdk/platform/xpcom",
"sdk/self": "sdk/self",
"sdk/system/unload": "sdk/system/unload"
},
"sectionName": "lib"
},
"lightbeam/shared/unload+": {
"docsSHA256": null,
"jsSHA256": "6e36c26b2cfbdf4dba923c7e7a384645487e9b435b6cd2ae61e2f91b0f90f789",
"moduleName": "shared/unload+",
"packageName": "lightbeam",
"requirements": {
"sdk/core/heritage": "sdk/core/heritage",
"sdk/core/namespace": "sdk/core/namespace",
"sdk/system/unload": "sdk/system/unload"
},
"sectionName": "lib"
},
"lightbeam/tab/utils": {
"docsSHA256": null,
"jsSHA256": "b8214407c3483cf4317c4971c64f20773290fe4310fb4485eced7697ac981501",
"moduleName": "tab/utils",
"packageName": "lightbeam",
"requirements": {
"chrome": "chrome",
"sdk/tabs": "sdk/tabs",
"sdk/tabs/utils": "sdk/tabs/utils"
},
"sectionName": "lib"
},
"lightbeam/ui": {
"docsSHA256": null,
"jsSHA256": "6eb39bb49339649c5fbf2c746ad94bcfa4f05f64e7c82903c73505c195e9aa2a",
"moduleName": "ui",
"packageName": "lightbeam",
"requirements": {
"./connection": "lightbeam/connection",
"./persist": "lightbeam/persist",
"sdk/preferences/service": "sdk/preferences/service",
"sdk/private-browsing": "sdk/private-browsing",
"sdk/self": "sdk/self",
"sdk/simple-prefs": "sdk/simple-prefs",
"sdk/simple-storage": "sdk/simple-storage",
"sdk/system": "sdk/system",
"sdk/system/xul-app": "sdk/system/xul-app",
"sdk/tabs": "sdk/tabs",
"sdk/ui/button/action": "sdk/ui/button/action",
"shared/menuitems": "lightbeam/shared/menuitems",
"shared/policy": "lightbeam/shared/policy"
},
"sectionName": "lib"
}
},
"metadata": {
"addon-sdk": {
"description": "Add-on development made easy.",
"keywords": [
"javascript",
"engine",
"addon",
"extension",
"xulrunner",
"firefox",
"browser"
],
"license": "MPL 2.0",
"name": "addon-sdk"
},
"lightbeam": {
"author": "Mozilla Foundation",
"description": "Lightbeam is a Firefox add-on that allows you to see the third parties that are collecting information about your browsing activity, with and without your consent. Using interactive visualizations, Lightbeam shows you the relationships between these third parties and the sites you visit.",
"license": "MPL 2.0",
"main": "main",
"name": "lightbeam",
"permissions": {
"private-browsing": true
},
"version": "1.2.1"
}
},
"name": "lightbeam",
"parseable": false,
"preferences": [
{
"description": "Graph or list",
"name": "defaultVisualization",
"options": [
{
"label": "Graph",
"value": "graph"
},
{
"label": "List",
"value": "list"
}
],
"title": "Default visualization",
"type": "menulist",
"value": "graph"
},
{
"description": "Time period",
"name": "defaultFilter",
"options": [
{
"label": "Daily",
"value": "daily"
},
{
"label": "Weekly",
"value": "weekly"
},
{
"label": "Recent",
"value": "recent"
},
{
"label": "Last 10 sites",
"value": "last10sites"
}
],
"title": "Default filter",
"type": "menulist",
"value": "daily"
}
],
"preferencesBranch": "jid1-F9UJ2thwoAm5gQ@jetpack",
"sdkVersion": "1.17",
"staticArgs": {},
"verbose": false
} resources/ 0000755 0000000 0000000 00000000000 02263405520 011564 5 ustar root root resources/lightbeam/ 0000755 0000000 0000000 00000000000 02263405520 013520 5 ustar root root resources/lightbeam/tests/ 0000755 0000000 0000000 00000000000 02263405520 014662 5 ustar root root resources/lightbeam/lib/ 0000755 0000000 0000000 00000000000 02263405520 014266 5 ustar root root resources/lightbeam/lib/ui.js 0000644 0000000 0000000 00000017726 12460353406 015263 0 ustar root root /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global require, exports, console */
'use strict';
const self = require("sdk/self");
const data = self.data;
const tabs = require('sdk/tabs');
const {
isPrivate
} = require("sdk/private-browsing");
const {
ContentPolicy
} = require('shared/policy');
const ss = require('sdk/simple-storage');
const prefs = require("sdk/simple-prefs").prefs;
const prefService = require("sdk/preferences/service");
const system = require("sdk/system");
const persist = require("./persist");
const {
Connection, getAllConnections
} = require("./connection");
const xulapp = require("sdk/system/xul-app");
const { ActionButton } =require("sdk/ui/button/action");
const mainPage = data.url("index.html");
var uiworker = null;
exports.mainPage = mainPage;
exports.attachToLightbeamPage = attachToLightbeamPage;
// These attach page workers to new tabs.
exports.onForWorker = function (eventname, handler) {
if (uiworker) {
uiworker.port.on(eventname, handler);
} else {
console.log('no uiworker to subscript to order');
}
};
exports.emitForWorker = function (eventname, obj) {
if (uiworker) {
uiworker.port.emit(eventname, obj);
}
};
// Begin tab handlers. These are for sidebar functionality, which is not
// present yet.
// FIXME: Move tab handlers into a tab component
// And specify what we're trying to do with it
function lightbeamTabClosed(tab) {
menuitem.label = "Show Lightbeam";
button.tooltip = "Show Lightbeam";
}
function lightbeamTabReady(tab) {
menuitem.label = "Close Lightbeam";
button.tooltip = "Close Lightbeam";
}
function lightbeamTabDeactivate(tab) {
menuitem.label = "Switch to Lightbeam Tab";
button.tooltip = "Switch to Lightbeam Tab";
}
const blockmap = ss.storage.blockmap;
var blocksites = Object.keys(blockmap);
console.log("blocking " + blocksites.length + ' sites');
// This is the heart of the Lightbeam blocking functionality.
ContentPolicy({
description: "Blocks user-defined blocklist from Lightbeam UI",
shouldLoad: function ({
location: location,
origin: origin
}) {
// ignore URIs with no host
var topLevelDomain;
try {
topLevelDomain = Connection.getDomain(location.host);
} catch (e) {
// See Issue 374: https://github.com/mozilla/lightbeam/issues/374
// if there is no host, like in about:what, then the host getter throws
return true;
}
if (blockmap[topLevelDomain]) {
return false;
}
return true;
}
});
function handlePrivateTab(tab) {
if (isPrivate(tab) && uiworker) {
// console.error('tab is private and uiworker exists');
uiworker.port.emit("private-browsing");
// console.error('sent message');
return true;
}
}
// if there is a private tab opened while the lightbeam tab is open,
// then alert the user about it.
tabs.on('open', handlePrivateTab);
// Notify the user in case they open a private window. Connections are
// visualized but never stored.
function hasPrivateTab() {
// console.error('hasPrivateTab: %s tabs to test', tabs.length);
for (var i = 0; i < tabs.length; i++) {
if (handlePrivateTab(tabs[i])) {
break; // the presence of a Private Window has been detected
}
}
}
// Connect the tab to the content script of the UI page. There may only ever be
// one UI page.
function attachToLightbeamPage(worker) {
console.debug("Attaching to lightbeam page");
uiworker = worker;
// The blocklist is maintained on both sides to reduce latency. However,
// this may cause sync errors.
function onWorkerUpdateBlocklist(site, blockFlag) {
if (blockFlag) {
if (!blockmap[site]) {
blockmap[site] = true;
}
} else {
if (blockmap[site]) {
delete blockmap[site];
}
}
uiworker.port.emit('update-blocklist', {
domain: site,
flag: blockFlag
});
}
function onBrowserPrefChanged(event) {
console.debug("Received updated browser prefs", JSON.stringify(event));
if ("trackingProtection" in event) {
prefService.set("privacy.trackingprotection.enabled", event.trackingProtection);
}
}
function onPrefChanged(event) {
console.debug("Received updated prefs", JSON.stringify(event));
if ("defaultVisualization" in event) {
prefs.defaultVisualization = event.defaultVisualization;
}
if ("defaultFilter" in event) {
prefs.defaultFilter = event.defaultFilter;
}
}
// Send over the the blocklist initially so we can use it.
worker.port.emit('update-blocklist-all',
Object.keys(blockmap).map(
function (site) {
return {
domain: site,
flag: blockmap[site]
};
}));
function onWorkerReset() {
// Reset buffered state
Connection.reset();
// And stored state, including prefs
persist.reset();
}
function onUIReady() {
worker.port.emit("updateUIFromMetadata", {
version: self.version,
browserVersion: system.version
});
worker.port.emit("updateUIFromBrowserPrefs", {
"trackingProtection": prefService.get("privacy.trackingprotection.enabled")
});
worker.port.emit("updateUIFromPrefs", prefs);
worker.port.emit("passStoredConnections", getAllConnections());
}
function onWorkerDetach() {
// console.error('detaching lightbeam view');
/* jshint validthis:true */
this.port.removeListener('reset', onWorkerReset);
this.port.removeListener('uiready', onUIReady);
this.port.removeListener('updateBlocklist', onWorkerUpdateBlocklist);
this.port.removeListener("browserPrefChanged", onBrowserPrefChanged);
this.port.removeListener("prefChanged", onPrefChanged);
uiworker = null;
this.destroy();
}
worker.on("detach", onWorkerDetach);
worker.port.on("reset", onWorkerReset);
worker.port.on('uiready', onUIReady);
worker.port.on('updateBlocklist', onWorkerUpdateBlocklist);
worker.port.on("browserPrefChanged", onBrowserPrefChanged);
worker.port.on("prefChanged", onPrefChanged);
worker.port.emit('init');
// if there is a private window open, then alert the user about it.
try {
hasPrivateTab();
} catch (e) {
console.error('Error testing with hasPrivateTab(): %o', e);
}
}
// This lets us toggle between the 3 states (no lightbeam tab open, lightbeam
// tab open but it's not the tab you're on, you're on the lightbeam tab)
function getLightbeamTab() {
for each(let tab in tabs) {
if (tab.url.slice(0, mainPage.length) === mainPage) {
return tab;
}
}
}
exports.getLightbeamTab = getLightbeamTab;
// Set up the menu item to open the main UI page:
var menuitem = require("shared/menuitems").Menuitem({
id: "lightbeam_openUITab",
menuid: "menu_ToolsPopup",
label: "Show Lightbeam",
onCommand: function () {
openOrSwitchToOrClose();
},
insertbefore: "sanitizeItem",
image: data.url("icons/lightbeam_logo-only_32x32.png")
});
function openOrSwitchToOrClose() {
// Open the Lightbeam tab, if it doesn't exist.
var tab = getLightbeamTab();
if (!tab) {
return tabs.open({
url: mainPage,
onOpen: lightbeamTabReady,
onClose: lightbeamTabClosed,
onReady: lightbeamTabReady,
onActivate: lightbeamTabReady,
onDeactivate: lightbeamTabDeactivate
});
}
// Close it if it's active.
if (tab === tabs.activeTab) {
tab.close();
} else {
// Otherwise, switch to the Lightbeam tab
tab.activate();
tab.window.activate();
}
}
exports.openOrSwitchToOrClose = openOrSwitchToOrClose;
// Set up the status bar button to open the main UI page:
var button = ActionButton({
id: "lightbeam_Widget",
label: "Lightbeam",
tooltip: "Show Lightbeam",
// Relative to the data directory
icon: {
"16": "./icons/lightbeam_logo-only_16x16.png",
"32": "./icons/lightbeam_logo-only_32x32.png",
"48": "./icons/lightbeam_logo-only_48x48.png",
},
onClick: function () {
openOrSwitchToOrClose();
}
});
resources/lightbeam/lib/tab/ 0000755 0000000 0000000 00000000000 02263405520 015034 5 ustar root root resources/lightbeam/lib/tab/utils.js 0000644 0000000 0000000 00000007325 12460353406 016546 0 ustar root root /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// ChromeTab
//
// This is a module for getting the tab a channel is loaded in, from the channel
exports.getTabForChannel = getTabForChannel;
exports.on = onTab;
exports.getTabInfo = getTabInfo;
const {
Cc, Ci, components
} = require('chrome');
const tabs = require('sdk/tabs');
const {
getTabForContentWindow
} = require('sdk/tabs/utils');
var wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
// return a variety of info on the tab
function getTabInfo(jpTab) {
// Some windows don't have performance initialized (because they haven't been reloaded since the plugin was initialized?
try {
var chromeWindow = wm.getMostRecentWindow('navigator:browser');
var gBrowser = chromeWindow.gBrowser;
var window = gBrowser.contentWindow.wrappedJSObject;
return {
gBrowser: gBrowser,
tab: gBrowser.selectedTab,
document: gBrowser.contentDocument,
window: window,
title: gBrowser.contentTitle, // nsIPrincipal
principal: gBrowser.contentPrincipal, // security context
uri: gBrowser.contentURI, // nsURI .spec to get string representation
loadTime: window.performance.timing.responseStart // milliseconds at which page load was initiated
};
} catch (e) {
return null;
}
}
function onTab(eventname, fn) {
tabs.on(eventname, function (jptab) {
var tabinfo = getTabInfo(jptab);
fn(tabinfo);
});
}
// Below code is based on adhacker, taken from http://forums.mozillazine.org/viewtopic.php?f=19&p=6335275
// Erik Vold may have the most current information on this.
function getTabForChannel(aHttpChannel) {
var loadContext = getLoadContext(aHttpChannel);
if (!loadContext) {
// fallback
return getTabForChannel2(aHttpChannel);
}
var win = loadContext.topWindow;
if (win) {
var tab = getTabForContentWindow(win);
// http://developer.mozilla.org/en/docs/XUL:tab
if (PrivateBrowsingUtils.isContentWindowPrivate) {
tab.isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(win);
} else {
tab.isPrivate = PrivateBrowsingUtils.isWindowPrivate(win); // ESR 31
}
return tab;
} else {
// console.error('getTabForChannel() no topWindow found');
return null;
}
}
// Special case in case we don't have a load context.
function getTabForChannel2(aChannel) {
var win = getWindowForChannel(aChannel);
if (!win) return null;
var tab = getTabForContentWindow(win);
return tab;
}
function getLoadContext(aRequest) {
try {
// first try the notification callbacks
var loadContext = aRequest.QueryInterface(Ci.nsIChannel)
.notificationCallbacks.getInterface(Ci.nsILoadContext);
return loadContext;
} catch (err1) {
// fail over to trying the load group
try {
if (!aRequest.loadGroup) return null;
var loadContext = aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
return loadContext;
} catch (err2) {
return null;
}
}
}
function getWindowForChannel(aRequest) {
var oHttp = aRequest.QueryInterface(Ci.nsIHttpChannel);
if (!oHttp.notificationCallbacks) {
console.log("HTTP request missing callbacks: " + oHttp.originalURI.spec);
return null;
}
var interfaceRequestor = oHttp.notificationCallbacks.QueryInterface(Ci.nsIInterfaceRequestor);
try {
return interfaceRequestor.getInterface(Ci.nsIDOMWindow);
} catch (e) {
console.log("Failed to to find nsIDOMWindow from interface requestor: " + e);
return null;
}
}
resources/lightbeam/lib/shared/ 0000755 0000000 0000000 00000000000 02263405520 015534 5 ustar root root resources/lightbeam/lib/shared/unload+.js 0000644 0000000 0000000 00000006222 12435776034 017446 0 ustar root root /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global require, console, exports */
'use strict';
const {
Class
} = require("sdk/core/heritage");
const unloadNS = require("sdk/core/namespace").ns();
const {
when: unload
} = require("sdk/system/unload");
var Unloader = exports.Unloader = Class({
initialize: function Unloader() {
unloadNS(this).unloaders = [];
unloadNS(this).unloadersUnload = unloadersUnload.bind(null, unloadNS(this).unloaders);
// run the unloaders on unload
unload(unloadNS(this).unloadersUnload);
},
unload: function unload(callback, container) {
// Calling with no arguments runs all the unloader callbacks
if (!callback) {
unloadNS(this).unloadersUnload();
return null;
}
let windowRemover = windowUnloader.bind(null, unloader, unloadNS(this).unloaders);
// The callback is bound to the lifetime of the container if we have one
if (container) {
// Remove the unloader when the container unloads
container.addEventListener("unload", windowRemover, false);
// Wrap the callback to additionally remove the unload listener
let origCallback = callback;
callback = function () {
container.removeEventListener("unload", windowRemover, false);
origCallback();
};
}
// Wrap the callback in a function that ignores failures
function unloader() {
try {
callback();
} catch (e) {
console.error(e);
}
}
unloadNS(this).unloaders.push(unloader);
// Provide a way to remove the unloader
return removeUnloader.bind(null, unloader, unloadNS(this).unloaders);
}
});
function sliceUnloader(unloader, unloaders) {
let index = unloaders.indexOf(unloader);
if (index < 0)
return [];
return unloaders.splice(index, 1);
}
// wraps sliceUnloader and doesn't return anything
function removeUnloader(unloader, unloaders) {
sliceUnloader.apply(null, arguments);
}
function windowUnloader(unloader, unloaders) {
sliceUnloader.apply(null, arguments).forEach(function (u) u());
}
function unloadersUnload(unloaders) {
// run all the pending unloaders
unloaders.slice().forEach(function (u) u());
// clear the unload array
unloaders.length = 0;
}
/**
* Save callbacks to run when unloading. Optionally scope the callback to a
* container, e.g., window. Provide a way to run all the callbacks.
*
* @usage unload(): Run all callbacks and release them.
*
* @usage unload(callback): Add a callback to run on unload.
* @param [function] callback: 0-parameter function to call on unload.
* @return [function]: A 0-parameter function that undoes adding the callback.
*
* @usage unload(callback, container) Add a scoped callback to run on unload.
* @param [function] callback: 0-parameter function to call on unload.
* @param [node] container: Remove the callback when this container unloads.
* @return [function]: A 0-parameter function that undoes adding the callback.
*/
const gUnload = Unloader();
exports.unload = gUnload.unload.bind(gUnload);
resources/lightbeam/lib/shared/policy.js 0000644 0000000 0000000 00000010367 12435776034 017415 0 ustar root root /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global require, exports */
'use strict';
const {
Cc, Ci
} = require('chrome');
const {
Class
} = require('sdk/core/heritage');
const CP_NS = require('sdk/core/namespace').ns();
const {
ensure
} = require('sdk/system/unload');
const {
validateOptions
} = require('sdk/deprecated/api-utils');
const {
id: ADDON_ID
} = require('sdk/self');
const xpcom = require('sdk/platform/xpcom');
const CM = Cc["@mozilla.org/categorymanager;1"]
.getService(Ci.nsICategoryManager);
const ACCEPT = exports.ACCEPT = Ci.nsIContentPolicy.ACCEPT;
const REJECT = exports.REJECT = Ci.nsIContentPolicy.REJECT_REQUEST;
const accept = function () ACCEPT;
let ContentPolicy_ID = 0;
const RULES = {
description: {
map: function (v) {
return v ? v : '';
},
is: ['string']
},
contract: {
map: function (v) {
if (v === undefined) {
v = '@erikvold.com/content-policy.' + ADDON_ID + ';' + ContentPolicy_ID++;
}
return v;
},
is: ['string']
},
entry: {
is: ['string', 'undefined']
},
shouldLoad: {
is: ['function', 'undefined']
},
shouldProcess: {
is: ['function', 'undefined']
},
};
function getType(aType) {
switch (aType) {
case Ci.nsIContentPolicy.TYPE_SCRIPT:
return 'script';
case Ci.nsIContentPolicy.TYPE_IMAGE:
return 'image';
case Ci.nsIContentPolicy.TYPE_STYLESHEET:
return 'stylesheet';
case Ci.nsIContentPolicy.TYPE_OBJECT:
return 'object';
case Ci.nsIContentPolicy.TYPE_DOCUMENT:
return 'document';
case Ci.nsIContentPolicy.TYPE_SUBDOCUMENT:
return 'subdocument';
case Ci.nsIContentPolicy.TYPE_REFRESH:
return 'refresh';
case Ci.nsIContentPolicy.TYPE_XBL:
return 'xbl';
case Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST:
return 'xhr';
case Ci.nsIContentPolicy.TYPE_PING:
return 'ping';
// TODO: support more types
}
return 'other';
}
const getTypeMemod = memoize(getType, 12, 1);
function makeDetails(aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess) {
return {
type: getTypeMemod(aContentType),
location: aContentLocation,
origin: aRequestOrigin.spec,
context: null, // TODO: support this in a safe way somehow..
mimeTypeGuess: String(aMimeTypeGuess)
};
}
let ContentPolicy = exports.ContentPolicy = Class({
initialize: function (options) {
const self = this;
options = CP_NS(self).options = validateOptions(options, RULES);
CP_NS(self).shouldLoad = options.shouldLoad || accept;
CP_NS(self).shouldProcess = options.shouldProcess || accept;
let factory = CP_NS(this).factory = xpcom.Factory({
Component: getProvider(self),
description: options.description,
contract: options.contract
});
let entry = options.entry || options.contract;
CM.addCategoryEntry('content-policy', entry, factory.contract, false, true);
ensure(this, 'destroy');
},
destroy: function () {
// already destroyed?
if (!CP_NS(this).options)
return;
let options = CP_NS(this).options;
CP_NS(this).options = null;
CP_NS(this).shouldLoad = accept;
CP_NS(this).shouldProcess = accept;
CM.deleteCategoryEntry('content-policy', options.entry || options.contract, false);
}
});
function getProvider(self) {
return Class({
extends: xpcom.Unknown,
interfaces: ['nsIContentPolicy'],
shouldLoad: function (aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess, aExtra) {
let load = CP_NS(self).shouldLoad(makeDetails.apply(null, arguments));
return (load == REJECT || (!load && load !== undefined)) ? REJECT : ACCEPT;
},
shouldProcess: function (aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess, aExtra) {
let load = CP_NS(self).shouldProcess(makeDetails.apply(null, arguments));
return (load == REJECT || (!load && load !== undefined)) ? REJECT : ACCEPT;
}
});
}
function memoize(func) {
let cache = Object.create(null);
return function (a) {
let key = a.toString();
if (!(key in cache)) {
cache[key] = func.call(null, a);
}
return cache[key];
};
}
resources/lightbeam/lib/shared/menuitems.js 0000644 0000000 0000000 00000014565 12445424176 020126 0 ustar root root /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global require, exports */
'use strict';
const windowUtils = require("sdk/deprecated/window-utils");
const {
Class
} = require("sdk/core/heritage");
const {
validateOptions
} = require("sdk/deprecated/api-utils");
const {
emit
} = require("sdk/event/core");
const {
isBrowser
} = require("sdk/window/utils");
const {
EventTarget
} = require('sdk/event/target');
const {
unload
} = require('./unload+');
const menuitemNS = require("sdk/core/namespace").ns();
const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
function MenuitemOptions(options) {
return validateOptions(options, {
id: {
is: ['string']
},
menuid: {
is: ['undefined', 'string']
},
insertbefore: {
is: ['undefined', 'string', 'object', 'number']
},
label: {
is: ["string"]
},
include: {
is: ['string', 'undefined']
},
disabled: {
is: ["undefined", "boolean"],
map: function (v) !! v
},
accesskey: {
is: ["undefined", "string"]
},
key: {
is: ["undefined", "string"]
},
checked: {
is: ['undefined', 'boolean']
},
className: {
is: ["undefined", "string"]
},
onCommand: {
is: ['undefined', 'function']
},
useChrome: {
map: function (v) !! v
}
});
}
let Menuitem = Class({
extends: EventTarget,
initialize: function (options) {
options = menuitemNS(this).options = MenuitemOptions(options);
EventTarget.prototype.initialize.call(this, options);
menuitemNS(this).destroyed = false;
menuitemNS(this).unloaders = [];
menuitemNS(this).menuitems = addMenuitems(this, options).menuitems;
},
get id() menuitemNS(this).options.id,
get label() menuitemNS(this).options.label,
set label(val) updateProperty(this, 'label', val),
get checked() menuitemNS(this).options.checked,
set checked(val) updateProperty(this, 'checked', !! val),
get disabled() menuitemNS(this).options.disabled,
set disabled(val) updateProperty(this, 'disabled', !! val),
get key() menuitemNS(this).options.key,
set key(val) updateProperty(this, 'key', val),
clone: function (overwrites) {
let opts = Object.clone(menuitemNS(this).options);
for (let key in overwrites) {
if (overwrites.hasOwnProperty(key)) {
opts[key] = overwrites[key];
}
}
return Menuitem(opts);
},
get menuid() menuitemNS(this).options.menuid,
set menuid(val) {
let options = menuitemNS(this).options;
options.menuid = val;
forEachMI(function (menuitem, i, $) {
updateMenuitemParent(menuitem, options, $);
});
},
destroy: function () {
if (!menuitemNS(this).destroyed) {
menuitemNS(this).destroyed = true;
menuitemNS(this).unloaders.forEach(function (u) u());
menuitemNS(this).unloaders = null;
menuitemNS(this).menuitems = null;
}
return true;
}
});
function addMenuitems(self, options) {
let menuitems = [];
// setup window tracker
windowUtils.WindowTracker({
onTrack: function (window) {
if (menuitemNS(self).destroyed) return;
if (options.include) {
if (options.include != window.location) return;
} else if (!isBrowser(window)) {
return;
}
// add the new menuitem to a menu
var menuitem = updateMenuitemAttributes(
window.document.createElementNS(NS_XUL, "menuitem"), options);
var menuitems_i = menuitems.push(menuitem) - 1;
// add the menutiem to the ui
let added = updateMenuitemParent(menuitem, options, function (id) window.document.getElementById(id));
menuitem.addEventListener("command", function () {
if (!self.disabled)
emit(self, 'command', options.useChrome ? window : null);
}, true);
// add unloader
let unloader = function unloader() {
if (menuitem.parentNode) {
menuitem.parentNode.removeChild(menuitem);
}
menuitems[menuitems_i] = null;
};
let remover = unload(unloader, window);
menuitemNS(self).unloaders.push(function () {
remover();
unloader();
});
}
});
return {
menuitems: menuitems
};
}
function updateMenuitemParent(menuitem, options, $) {
// add the menutiem to the ui
if (Array.isArray(options.menuid)) {
let ids = options.menuid;
for (var len = ids.length, i = 0; i < len; i++) {
if (tryParent($(ids[i]), menuitem, options.insertbefore))
return true;
}
} else {
return tryParent($(options.menuid), menuitem, options.insertbefore);
}
return false;
}
function updateMenuitemAttributes(menuitem, options) {
menuitem.setAttribute("id", options.id);
menuitem.setAttribute("label", options.label);
if (options.accesskey)
menuitem.setAttribute("accesskey", options.accesskey);
if (options.key)
menuitem.setAttribute("key", options.key);
menuitem.setAttribute("disabled", !! options.disabled);
if (options.image) {
menuitem.classList.add("menuitem-iconic");
menuitem.style.listStyleImage = "url('" + options.image + "')";
}
if (options.checked)
menuitem.setAttribute('checked', options.checked);
if (options.className)
options.className.split(/\s+/).forEach(function (name) menuitem.classList.add(name));
return menuitem;
}
function updateProperty(menuitem, key, val) {
menuitemNS(menuitem).options[key] = val;
forEachMI(function (menuitem) {
menuitem.setAttribute(key, val);
}, menuitem);
return val;
}
function forEachMI(callback, menuitem) {
menuitemNS(menuitem).menuitems.forEach(function (mi, i) {
if (!mi) return;
callback(mi, i, function (id) mi.ownerDocument.getElementById(id));
});
}
function tryParent(parent, menuitem, before) {
if (parent) parent.insertBefore(menuitem, insertBefore(parent, before));
return !!parent;
}
function insertBefore(parent, before) {
if (typeof before == "number") {
switch (before) {
case MenuitemExport.FIRST_CHILD:
return parent.firstChild;
}
return null;
} else if (typeof before == "string") {
return parent.querySelector("#" + before);
}
return before;
}
function MenuitemExport(options) {
return Menuitem(options);
}
MenuitemExport.FIRST_CHILD = 1;
exports.Menuitem = MenuitemExport;
resources/lightbeam/lib/persist.js 0000644 0000000 0000000 00000004473 12445424176 016340 0 ustar root root /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global console, require, exports */
// All writes to storage in the addon process goes here.
"use strict";
const ss = require('sdk/simple-storage');
const Request = require("sdk/request").Request;
const prefs = require("sdk/simple-prefs").prefs;
var storage = ss.storage;
// Only these keys may exist as maps on ss.storage. Everything else is legacy.
const STORAGE_KEYS = [
"blockmap",
"connections",
];
// Delete oldest connections. When we hit the simple storage quota limit,
// Firefox throws an exception that the user won't see. We tried switching to
// indexdb (which is async) but abandoned it. localForage may be a viable
// substitute.
function checkStorageQuota() {
while (ss.quotaUsage > 1) {
var sliceStart = ss.storage.connections.length / 2;
ss.storage.connections = ss.storage.connections.slice(sliceStart);
}
}
// Flush connections to simple-storage.
exports.storeConnections = function storeConnections(connections) {
checkStorageQuota();
storage.connections = storage.connections.concat(connections);
};
// Reset stored state, including preferences
exports.reset = function reset() {
storage.connections.length = 0;
storage.blockmap = {};
storage.userId = generateUserId();
prefs.defaultVisualization = "graph";
prefs.defaultFilter = "daily";
};
// Generate a new user id.
function generateUserId() {
// Short hex string.
let userId = Math.floor(0xFFFFFFFF * Math.random()).toString(16);
storage.userId = userId + ":" + Date.now();
return storage.userId;
}
// Possibly rotate the user id.
function maybeRotateUserId(forceChange) {
let parts = storage.userId.split(":");
// 90 days in ms
let MAX_LIFETIME_MS = 90 * 24 * 60 * 60 * 1000;
let timeToChange = Date(parts[1] + MAX_LIFETIME_MS);
if (forceChange || Date.now() >= timeToChange) {
generateUserId();
}
}
// Initialize all of our storage
if (!storage.connections) {
storage.connections = [];
}
if (!storage.blockmap) {
storage.blockmap = {};
}
if (!storage.userId) {
generateUserId();
}
// Rotate user id if necessary
maybeRotateUserId();
console.log('Current quota usage:', Math.round(ss.quotaUsage * 100));
resources/lightbeam/lib/main.js 0000644 0000000 0000000 00000004452 12463022526 015561 0 ustar root root /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global exports, require */
"use strict";
// require("sdk/preferences/service").set("extensions.sdk.console.logLevel", "debug");
const events = require("sdk/system/events");
const {
PageMod
} = require("sdk/page-mod");
const {
data
} = require("sdk/self");
const {
Connection, addConnection
} = require('./connection');
const ui = require('./ui');
var tabs = require("sdk/tabs");
// This is the heart of Lightbeam, we get all of our data from observing these
// requests.
events.on("http-on-examine-response", function (subject) {
var connection = Connection.fromSubject(subject);
if (connection.valid) {
addConnection(connection);
// Pass the message on to the UI
ui.emitForWorker('connection', connection.toLog());
}
}, true);
// This lets us hook into page load events and communicate to page workers.
PageMod({
include: ui.mainPage,
contentScriptWhen: 'ready',
contentScriptFile: [
data.url('content-script.js'),
data.url('d3/d3.js'),
data.url('events.js'),
data.url('infobar.js'),
data.url('lightbeam.js'),
data.url('svgdataset.js'),
data.url('aggregate.js'),
data.url('picoModal/picoModal.js'),
data.url('tooltip.js'),
data.url('dialog.js'),
data.url('ui.js'),
data.url('parseuri.js'),
data.url('graph.js'),
data.url('list.js'),
],
onAttach: ui.attachToLightbeamPage
});
exports.main = function (options, callbacks) {
let initialPage = null;
switch (options.loadReason) {
case "install":
initialPage = "first-run.html";
break;
//case "upgrade":
// initialPage = "upgrade.html";
// break;
}
if (initialPage) {
let initialPageUrl = data.url(initialPage);
tabs.open({url: initialPageUrl,
inBackground: true});
// Add a content script to open lightbeam if the corresponding button is
// pressed in the inital page
PageMod({
include: initialPageUrl,
contentScriptWhen: 'ready',
contentScriptFile: data.url('initialPage.js'),
onAttach: function (worker) {
worker.port.on('openLightbeam', ui.openOrSwitchToOrClose);
}
});
}
};
resources/lightbeam/lib/connection.js 0000644 0000000 0000000 00000022702 12445424176 017001 0 ustar root root /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Connection object. This module may try to do too many things.
//
// Convert an HTTP request (channel) to a loggable, visualizable connection
// object, if possible
const {
Cc, Ci, Cr
} = require('chrome');
const {
on, off, emit
} = require('sdk/event/core');
const {
data
} = require("sdk/self");
const addonUrl = data.url("index.html");
const persist = require("./persist");
const {
setTimeout
} = require("sdk/timers");
const ss = require('sdk/simple-storage');
// An array of connection objects serialized to an array.
var connBatch = [];
const connBatchSize = 200;
var eTLDSvc = Cc["@mozilla.org/network/effective-tld-service;1"].
getService(Ci.nsIEffectiveTLDService);
const {
getTabForChannel
} = require('./tab/utils');
exports.Connection = Connection;
exports.addConnection = addConnection;
function addConnection(connection) {
connBatch.push(connection.toLog());
if (connBatch.length == connBatchSize) {
flushToStorage();
}
console.debug("got", connBatch.length, "connections");
}
exports.getAllConnections = function getAllConnections() {
console.debug("got", connBatch.length, "buffered connections", ss.storage.connections.length, "persisted connections");
return ss.storage.connections.concat(connBatch);
};
function excludePrivateConnections(connections) {
return connections.filter(function (connection) {
return !connection[Connection.FROM_PRIVATE_MODE];
});
}
function flushToStorage() {
console.debug("flushing", connBatch.length, "buffered connections");
persist.storeConnections(excludePrivateConnections(connBatch));
connBatch.length = 0;
}
// Every 5 minutes, flush to storage.
setTimeout(flushToStorage, 5 * 60 * 1000);
// Get eTLD + 1 (e.g., example.com from foo.example.com)
function getDomain(host) {
try {
return eTLDSvc.getBaseDomainFromHost(host);
} catch (e
if e.result === Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
return false;
} catch (e
if e.result === Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
return false;
} catch (e) {
console.error('getDomain(): unexpected error: ' + host + ': ' + e);
throw e;
}
}
Connection.getDomain = getDomain; // make it part of what we export
Connection.reset = function () {
connBatch.length = 0;
};
// Get subdomain (e.g., foo from foo.example.com)
function getSubdomain(host) {
var domain = getDomain(host);
return host.slice(0, host.length - domain.length);
}
function Connection() {}
// subject comes from events.on("http-on-modify-request");
Connection.fromSubject = function (subject) {
var conn = new Connection();
conn.restoreFromSubject(subject);
return conn;
};
// Functions below may include legacy code from the first version of Collusion.
// Find the page the URL was originally loaded from to determine whether this
// happened in first or third-party context.
function getAjaxRequestHeader(channel) {
var header = null;
try {
header = channel.getRequestHeader('X-Requested-With').toLowerCase() === 'xmlhttprequest';
} catch (e) {
if (e.name === 'NS_ERROR_NOT_AVAILABLE') {
/* header not found, do nothing */
} else {
console.error('what is this? ' + Object.keys(e).join(','));
throw e;
}
}
return header;
}
// Does this make sense from http-on-examine-response? The response will have
// Set-Cookie headers, the request will have Cookie headers. We should
// investigate nsIHttpChannel to make sure that the response channel has the
// original request headers.
function hasCookie(channel) {
try {
return !!channel.getRequestHeader('Cookie');
} catch (e) {
return false;
}
}
function getProtocol(uri) {
return uri.scheme;
}
// See doc/Heuristic for determining first party-ness.md
Connection.prototype.restoreFromSubject = function (event) {
// Check to see if this is in fact a third-party connection, if not, return
var channel = event.subject.QueryInterface(Ci.nsIHttpChannel);
// The referrer may not be set properly, especially in the case where HTTPS
// includes HTTP where the referrer is stripped due to mixed-content
// attacks. Also it doesn't work for RTC or WebSockets, but since we are
// only examining HTTP requests right now, that's all right.
var source = channel.referrer;
var target = channel.URI;
var targetDomain = getDomain(target.host);
var tab = null;
try {
tab = getTabForChannel(channel);
} catch (e) {
console.debug('EXCEPTION CAUGHT: No tab for connection');
tab = null;
}
var isAjax = getAjaxRequestHeader(channel);
var valid = true;
var browserUri = tab ? tab.linkedBrowser.currentURI : null;
var browserSpec = browserUri && browserUri.spec;
var browserDomain = null;
try {
browserDomain = browserUri && getDomain(browserUri.host);
} catch (e) {
// chances are the URL is about:blank, which has no host and throws an exception
// console.error('Error getting host from: ' + browserUri.spec);
}
// This is probably the largest source of false positives.
var sourceVisited = !isAjax && (browserDomain === targetDomain || browserSpec === 'about:blank');
// Connection.log('browserUri ' + browserUri.spec + (sourceVisited ? ' equals ' : ' does not equal') + ' target ' + ( target && target.spec));
if (sourceVisited) {
source = target;
} else if (!source) {
// This may introduce faulty data.
// console.error('No source for target ' + target.spec + ' (' + channel.referrer + ')');
source = target; // can't have a null source
}
var sourceDomain = getDomain(source.host);
var cookie = hasCookie(channel);
var sourceProtocol = getProtocol(source);
var targetProtocol = getProtocol(target);
var isSecure = targetProtocol === 'https';
var isPrivate = tab && tab.isPrivate;
// Handle all failure conditions
if (browserUri && browserUri.spec === addonUrl) {
this.valid = false;
this.message = 'Do not record connections made by this add-on';
// console.error(this.message);
return this;
}
if (!sourceDomain) {
this.valid = false;
this.message = 'Invalid source domain: ' + source.host;
// console.error(this.message);
return this;
}
if (!targetDomain) {
this.valid = false;
this.message = 'Invalid target domain: ' + target.host;
// console.error(this.message);
return this;
}
if (target.host === 'localhost' || source.host === 'localhost') {
this.valid = false;
this.message = 'Localhost is not trackable';
// console.error(this.message);
return this;
}
if (sourceProtocol === 'http' || sourceProtocol === 'https' ||
targetProtocol === 'http' || targetProtocol === 'https') {
/* OK, do nothing */
} else {
this.valid = false;
this.message = 'Unsupported protocol: ' + sourceProtocol + ' -> ' + targetProtocol;
// console.error(this.message);
return this;
}
if (!tab) {
this.valid = false;
this.message = 'No tab found for request: ' + target.spec + ', isAjax: ' + isAjax;
// console.error(this.message);
return this;
}
// set instance values for return
this.valid = true;
this.source = sourceDomain;
this.target = targetDomain;
this.timestamp = Date.now();
this.contentType = channel.contentType || 'text/plain';
this.cookie = cookie;
// The client went to this page in a first-party context.
this.sourceVisited = sourceVisited;
this.secure = isSecure;
this.sourcePathDepth = source.path.split('/').length - 1;
this.sourceQueryDepth = source.query ? target.query.split(/;|\&/).length : 0;
this.sourceSub = getSubdomain(source.host);
this.targetSub = getSubdomain(target.host);
this.method = channel.requestMethod;
this.status = channel.responseStatus;
this.cacheable = !channel.isNoCacheResponse();
// We visualize private connections but never store them.
this.isPrivate = isPrivate;
this._sourceTab = tab; // Never logged, only for associating data with current tab
// console.error((sourceVisited ? 'site: ' : 'tracker: ') + sourceDomain + ' -> ' + targetDomain + ' (' + browserUri.spec + ')');
};
// Connection - level methods (not on instances)
// This may be supported by the addon-sdk events.on now.
Connection.on = function (eventname, handler) {
on(Connection, eventname, handler);
};
Connection.off = function (eventname) {
off(Connection, eventname);
};
Connection.emit = function (eventname, arg1, arg2, arg3) {
emit(Connection, eventname, arg1, arg2, arg3);
};
// Constants for indexes of properties in array format
Connection.SOURCE = 0;
Connection.TARGET = 1;
Connection.TIMESTAMP = 2;
Connection.CONTENT_TYPE = 3;
Connection.COOKIE = 4;
Connection.SOURCE_VISITED = 5;
Connection.SECURE = 6;
Connection.SOURCE_PATH_DEPTH = 7;
Connection.SOURCE_QUERY_DEPTH = 8;
Connection.SOURCE_SUB = 9;
Connection.TARGET_SUB = 10;
Connection.METHOD = 11;
Connection.STATUS = 12;
Connection.CACHEABLE = 13;
Connection.FROM_PRIVATE_MODE = 14;
Connection.prototype.toLog = function () {
if (!this.valid) {
throw new Error('Do not log invalid connections: ' + this.message);
}
var theLog = [
this.source,
this.target,
this.timestamp.valueOf(),
this.contentType,
this.cookie,
this.sourceVisited,
this.secure,
this.sourcePathDepth,
this.sourceQueryDepth,
this.sourceSub,
this.targetSub,
this.method,
this.status,
this.cacheable,
this._sourceTab.isPrivate
];
if (this.isPrivate) {
theLog.push(this.isPrivate);
}
return theLog;
};
resources/lightbeam/data/ 0000755 0000000 0000000 00000000000 02263405520 014431 5 ustar root root resources/lightbeam/data/upgrade.html 0000644 0000000 0000000 00000003767 12457355132 016774 0 ustar root root
Welcome to Lightbeam!
Thanks for upgrading Lightbeam!
Major changes this release include an updated
data format, refactoring the content scripts to omit references to unsafeWindow,
and refactoring storage. Due to storage changes, previously stored
connections will be wiped out with this upgrade. We expect users'
connections graph to be restored to their normal state in a few days, since
quota limits restrict the amount of data stored anyway. For a complete list
of changes, please visit our github
repository.
First-time contributors for this release include Francois Marier, Spyros Livathinos, and Andrew William-Smith. We are very fortunate to have such wonderful contributors in our community.
Learn more about Mozilla's Lightbeam for Firefox on our project page