Local packaging for firefox add-on.
authorMark Donnelly <mark@painless-security.com>
Wed, 13 May 2015 10:34:37 +0000 (06:34 -0400)
committerMark Donnelly <mark@painless-security.com>
Wed, 13 May 2015 10:34:37 +0000 (06:34 -0400)
The firefox add-on used to be dependant on the Mozilla add-on SDK, but this SDK did not work with the build process.  Instead, we have now added in the files from the addon
SDK packaging, and use a system 'zip' function to bundle it all up.

browsers/firefox/CMakeLists.txt
browsers/firefox/packaging/bootstrap.js [new file with mode: 0644]
browsers/firefox/packaging/defaults/preferences/prefs.js [new file with mode: 0644]
browsers/firefox/packaging/harness-options.json [new file with mode: 0644]
browsers/firefox/packaging/install.rdf [new file with mode: 0644]
browsers/firefox/packaging/locales.json [new file with mode: 0644]

index f034f76..d43d98a 100644 (file)
@@ -1,10 +1,14 @@
 cmake_minimum_required(VERSION 2.8)
 
 add_custom_target( gssweb.xpi ALL
-  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/package.json package.json
-  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/chrome.manifest chrome.manifest
-  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/lib/main.js lib/main.js
+  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/packaging/locales.json locales.json
+  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/lib/main.js resources/gssweb/lib/main.js
+  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/../common/contentscript.js resources/gssweb/data/contentscript.js
+  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/packaging/install.rdf install.rdf
+  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/packaging/harness-options.json harness-options.json
+  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/packaging/bootstrap.js bootstrap.js
+  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/packaging/defaults/preferences/prefs.js defaults/preferences/prefs.js
   COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/../common/navigator.gssEap.js chrome/content/navigator.gssEap.js
-  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/../common/contentscript.js data/contentscript.js
-  COMMAND cfx xpi 
+  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/chrome.manifest chrome.manifest
+  COMMAND zip gssweb.xpi locales.json resources/gssweb/lib/main.js ./resources/gssweb/data/contentscript.js ./install.rdf ./harness-options.json ./bootstrap.js ./defaults/preferences/prefs.js ./chrome/content/navigator.gssEap.js ./chrome.manifest
 )
diff --git a/browsers/firefox/packaging/bootstrap.js b/browsers/firefox/packaging/bootstrap.js
new file mode 100644 (file)
index 0000000..6bb014a
--- /dev/null
@@ -0,0 +1,337 @@
+/* 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/. */
+
+// @see http://mxr.mozilla.org/mozilla-central/source/js/src/xpconnect/loader/mozJSComponentLoader.cpp
+
+'use strict';
+
+// IMPORTANT: Avoid adding any initialization tasks here, if you need to do
+// something before add-on is loaded consider addon/runner module instead!
+
+const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu,
+        results: Cr, manager: Cm } = Components;
+const ioService = Cc['@mozilla.org/network/io-service;1'].
+                  getService(Ci.nsIIOService);
+const resourceHandler = ioService.getProtocolHandler('resource').
+                        QueryInterface(Ci.nsIResProtocolHandler);
+const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
+const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1'].
+                     getService(Ci.mozIJSSubScriptLoader);
+const prefService = Cc['@mozilla.org/preferences-service;1'].
+                    getService(Ci.nsIPrefService).
+                    QueryInterface(Ci.nsIPrefBranch);
+const appInfo = Cc["@mozilla.org/xre/app-info;1"].
+                getService(Ci.nsIXULAppInfo);
+const vc = Cc["@mozilla.org/xpcom/version-comparator;1"].
+           getService(Ci.nsIVersionComparator);
+
+
+const REASON = [ 'unknown', 'startup', 'shutdown', 'enable', 'disable',
+                 'install', 'uninstall', 'upgrade', 'downgrade' ];
+
+const bind = Function.call.bind(Function.bind);
+
+let loader = null;
+let unload = null;
+let cuddlefishSandbox = null;
+let nukeTimer = null;
+
+// Utility function that synchronously reads local resource from the given
+// `uri` and returns content string.
+function readURI(uri) {
+  let ioservice = Cc['@mozilla.org/network/io-service;1'].
+    getService(Ci.nsIIOService);
+  let channel = ioservice.newChannel(uri, 'UTF-8', null);
+  let stream = channel.open();
+
+  let cstream = Cc['@mozilla.org/intl/converter-input-stream;1'].
+    createInstance(Ci.nsIConverterInputStream);
+  cstream.init(stream, 'UTF-8', 0, 0);
+
+  let str = {};
+  let data = '';
+  let read = 0;
+  do {
+    read = cstream.readString(0xffffffff, str);
+    data += str.value;
+  } while (read != 0);
+
+  cstream.close();
+
+  return data;
+}
+
+// We don't do anything on install & uninstall yet, but in a future
+// we should allow add-ons to cleanup after uninstall.
+function install(data, reason) {}
+function uninstall(data, reason) {}
+
+function startup(data, reasonCode) {
+  try {
+    let reason = REASON[reasonCode];
+    // URI for the root of the XPI file.
+    // 'jar:' URI if the addon is packed, 'file:' URI otherwise.
+    // (Used by l10n module in order to fetch `locale` folder)
+    let rootURI = data.resourceURI.spec;
+
+    // TODO: Maybe we should perform read harness-options.json asynchronously,
+    // since we can't do anything until 'sessionstore-windows-restored' anyway.
+    let options = JSON.parse(readURI(rootURI + './harness-options.json'));
+
+    let id = options.jetpackID;
+    let name = options.name;
+
+    // Clean the metadata
+    options.metadata[name]['permissions'] = options.metadata[name]['permissions'] || {};
+
+    // freeze the permissionss
+    Object.freeze(options.metadata[name]['permissions']);
+    // freeze the metadata
+    Object.freeze(options.metadata[name]);
+
+    // Register a new resource 'domain' for this addon which is mapping to
+    // XPI's `resources` folder.
+    // Generate the domain name by using jetpack ID, which is the extension ID
+    // by stripping common characters that doesn't work as a domain name:
+    let uuidRe =
+      /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/;
+
+    let domain = id.
+      toLowerCase().
+      replace(/@/g, '-at-').
+      replace(/\./g, '-dot-').
+      replace(uuidRe, '$1');
+
+    let prefixURI = 'resource://' + domain + '/';
+    let resourcesURI = ioService.newURI(rootURI + '/resources/', null, null);
+    resourceHandler.setSubstitution(domain, resourcesURI);
+
+    // Create path to URLs mapping supported by loader.
+    let paths = {
+      // Relative modules resolve to add-on package lib
+      './': prefixURI + name + '/lib/',
+      './tests/': prefixURI + name + '/tests/',
+      '': 'resource://gre/modules/commonjs/'
+    };
+
+    // Maps addon lib and tests ressource folders for each package
+    paths = Object.keys(options.metadata).reduce(function(result, name) {
+      result[name + '/'] = prefixURI + name + '/lib/'
+      result[name + '/tests/'] = prefixURI + name + '/tests/'
+      return result;
+    }, paths);
+
+    // We need to map tests folder when we run sdk tests whose package name
+    // is stripped
+    if (name == 'addon-sdk')
+      paths['tests/'] = prefixURI + name + '/tests/';
+
+    let useBundledSDK = options['force-use-bundled-sdk'];
+    if (!useBundledSDK) {
+      try {
+        useBundledSDK = prefService.getBoolPref("extensions.addon-sdk.useBundledSDK");
+      }
+      catch (e) {
+        // Pref doesn't exist, allow using Firefox shipped SDK
+      }
+    }
+
+    // Starting with Firefox 21.0a1, we start using modules shipped into firefox
+    // Still allow using modules from the xpi if the manifest tell us to do so.
+    // And only try to look for sdk modules in xpi if the xpi actually ship them
+    if (options['is-sdk-bundled'] &&
+        (vc.compare(appInfo.version, '21.0a1') < 0 || useBundledSDK)) {
+      // Maps sdk module folders to their resource folder
+      paths[''] = prefixURI + 'addon-sdk/lib/';
+      // test.js is usually found in root commonjs or SDK_ROOT/lib/ folder,
+      // so that it isn't shipped in the xpi. Keep a copy of it in sdk/ folder
+      // until we no longer support SDK modules in XPI:
+      paths['test'] = prefixURI + 'addon-sdk/lib/sdk/test.js';
+    }
+
+    // Retrieve list of module folder overloads based on preferences in order to
+    // eventually used a local modules instead of files shipped into Firefox.
+    let branch = prefService.getBranch('extensions.modules.' + id + '.path');
+    paths = branch.getChildList('', {}).reduce(function (result, name) {
+      // Allows overloading of any sub folder by replacing . by / in pref name
+      let path = name.substr(1).split('.').join('/');
+      // Only accept overloading folder by ensuring always ending with `/`
+      if (path) path += '/';
+      let fileURI = branch.getCharPref(name);
+
+      // On mobile, file URI has to end with a `/` otherwise, setSubstitution
+      // takes the parent folder instead.
+      if (fileURI[fileURI.length-1] !== '/')
+        fileURI += '/';
+
+      // Maps the given file:// URI to a resource:// in order to avoid various
+      // failure that happens with file:// URI and be close to production env
+      let resourcesURI = ioService.newURI(fileURI, null, null);
+      let resName = 'extensions.modules.' + domain + '.commonjs.path' + name;
+      resourceHandler.setSubstitution(resName, resourcesURI);
+
+      result[path] = 'resource://' + resName + '/';
+      return result;
+    }, paths);
+
+    // Make version 2 of the manifest
+    let manifest = options.manifest;
+
+    // Import `cuddlefish.js` module using a Sandbox and bootstrap loader.
+    let cuddlefishPath = 'loader/cuddlefish.js';
+    let cuddlefishURI = 'resource://gre/modules/commonjs/sdk/' + cuddlefishPath;
+    if (paths['sdk/']) { // sdk folder has been overloaded
+                         // (from pref, or cuddlefish is still in the xpi)
+      cuddlefishURI = paths['sdk/'] + cuddlefishPath;
+    }
+    else if (paths['']) { // root modules folder has been overloaded
+      cuddlefishURI = paths[''] + 'sdk/' + cuddlefishPath;
+    }
+
+    cuddlefishSandbox = loadSandbox(cuddlefishURI);
+    let cuddlefish = cuddlefishSandbox.exports;
+
+    // Normalize `options.mainPath` so that it looks like one that will come
+    // in a new version of linker.
+    let main = options.mainPath;
+
+    unload = cuddlefish.unload;
+    loader = cuddlefish.Loader({
+      paths: paths,
+      // modules manifest.
+      manifest: manifest,
+
+      // Add-on ID used by different APIs as a unique identifier.
+      id: id,
+      // Add-on name.
+      name: name,
+      // Add-on version.
+      version: options.metadata[name].version,
+      // Add-on package descriptor.
+      metadata: options.metadata[name],
+      // Add-on load reason.
+      loadReason: reason,
+
+      prefixURI: prefixURI,
+      // Add-on URI.
+      rootURI: rootURI,
+      // options used by system module.
+      // File to write 'OK' or 'FAIL' (exit code emulation).
+      resultFile: options.resultFile,
+      // Arguments passed as --static-args
+      staticArgs: options.staticArgs,
+
+      // Arguments related to test runner.
+      modules: {
+        '@test/options': {
+          allTestModules: options.allTestModules,
+          iterations: options.iterations,
+          filter: options.filter,
+          profileMemory: options.profileMemory,
+          stopOnError: options.stopOnError,
+          verbose: options.verbose,
+          parseable: options.parseable,
+          checkMemory: options.check_memory,
+        }
+      }
+    });
+
+    let module = cuddlefish.Module('sdk/loader/cuddlefish', cuddlefishURI);
+    let require = cuddlefish.Require(loader, module);
+
+    require('sdk/addon/runner').startup(reason, {
+      loader: loader,
+      main: main,
+      prefsURI: rootURI + 'defaults/preferences/prefs.js'
+    });
+  } catch (error) {
+    dump('Bootstrap error: ' +
+         (error.message ? error.message : String(error)) + '\n' +
+         (error.stack || error.fileName + ': ' + error.lineNumber) + '\n');
+    throw error;
+  }
+};
+
+function loadSandbox(uri) {
+  let proto = {
+    sandboxPrototype: {
+      loadSandbox: loadSandbox,
+      ChromeWorker: ChromeWorker
+    }
+  };
+  let sandbox = Cu.Sandbox(systemPrincipal, proto);
+  // Create a fake commonjs environnement just to enable loading loader.js
+  // correctly
+  sandbox.exports = {};
+  sandbox.module = { uri: uri, exports: sandbox.exports };
+  sandbox.require = function (id) {
+    if (id !== "chrome")
+      throw new Error("Bootstrap sandbox `require` method isn't implemented.");
+
+    return Object.freeze({ Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm,
+      CC: bind(CC, Components), components: Components,
+      ChromeWorker: ChromeWorker });
+  };
+  scriptLoader.loadSubScript(uri, sandbox, 'UTF-8');
+  return sandbox;
+}
+
+function unloadSandbox(sandbox) {
+  if ("nukeSandbox" in Cu)
+    Cu.nukeSandbox(sandbox);
+}
+
+function setTimeout(callback, delay) {
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.initWithCallback({ notify: callback }, delay,
+                         Ci.nsITimer.TYPE_ONE_SHOT);
+  return timer;
+}
+
+function shutdown(data, reasonCode) {
+  let reason = REASON[reasonCode];
+  if (loader) {
+    unload(loader, reason);
+    unload = null;
+
+    // Don't waste time cleaning up if the application is shutting down
+    if (reason != "shutdown") {
+      // Avoid leaking all modules when something goes wrong with one particular
+      // module. Do not clean it up immediatly in order to allow executing some
+      // actions on addon disabling.
+      // We need to keep a reference to the timer, otherwise it is collected
+      // and won't ever fire.
+      nukeTimer = setTimeout(nukeModules, 1000);
+    }
+  }
+};
+
+function nukeModules() {
+  nukeTimer = null;
+  // module objects store `exports` which comes from sandboxes
+  // We should avoid keeping link to these object to avoid leaking sandboxes
+  for (let key in loader.modules) {
+    delete loader.modules[key];
+  }
+  // Direct links to sandboxes should be removed too
+  for (let key in loader.sandboxes) {
+    let sandbox = loader.sandboxes[key];
+    delete loader.sandboxes[key];
+    // Bug 775067: From FF17 we can kill all CCW from a given sandbox
+    unloadSandbox(sandbox);
+  }
+  loader = null;
+
+  // both `toolkit/loader` and `system/xul-app` are loaded as JSM's via
+  // `cuddlefish.js`, and needs to be unloaded to avoid memory leaks, when
+  // the addon is unload.
+
+  unloadSandbox(cuddlefishSandbox.loaderSandbox);
+  unloadSandbox(cuddlefishSandbox.xulappSandbox);
+
+  // Bug 764840: We need to unload cuddlefish otherwise it will stay alive
+  // and keep a reference to this compartment.
+  unloadSandbox(cuddlefishSandbox);
+  cuddlefishSandbox = null;
+}
diff --git a/browsers/firefox/packaging/defaults/preferences/prefs.js b/browsers/firefox/packaging/defaults/preferences/prefs.js
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/browsers/firefox/packaging/harness-options.json b/browsers/firefox/packaging/harness-options.json
new file mode 100644 (file)
index 0000000..43b45d1
--- /dev/null
@@ -0,0 +1,53 @@
+{
+ "check_memory": false, 
+ "enable_e10s": false, 
+ "is-sdk-bundled": false, 
+ "jetpackID": "jid1-NfTB1x7j36TqCw@jetpack", 
+ "loader": "addon-sdk/lib/sdk/loader/cuddlefish.js", 
+ "main": "main", 
+ "mainPath": "gssweb/main", 
+ "manifest": {
+  "gssweb/main": {
+   "docsSHA256": null, 
+   "jsSHA256": "8c055bdc497f77e3f3ec096e00e2c7b5caacc2b66e37ae9542875e8a014a6b1e", 
+   "moduleName": "main", 
+   "packageName": "gssweb", 
+   "requirements": {
+    "chrome": "chrome", 
+    "sdk/page-mod": "sdk/page-mod", 
+    "sdk/self": "sdk/self", 
+    "sdk/tabs": "sdk/tabs"
+   }, 
+   "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"
+  }, 
+  "gssweb": {
+   "author": "mark@painless-security.com", 
+   "description": "GSS-EAP and GSSWeb package provider", 
+   "license": "Copyright (c) 2015, JANET(UK)\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n   notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n\n3. Neither the name of JANET(UK) nor the names of its contributors\n   may be used to endorse or promote products derived from this software\n   without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS\nFOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,\nINDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\nHOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\nSTRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED\nOF THE POSSIBILITY OF SUCH DAMAGE.", 
+   "main": "main", 
+   "name": "gssweb", 
+   "version": "0.1"
+  }
+ }, 
+ "name": "gssweb", 
+ "parseable": false, 
+ "sdkVersion": "unknown", 
+ "staticArgs": {}, 
+ "verbose": false
+}
\ No newline at end of file
diff --git a/browsers/firefox/packaging/install.rdf b/browsers/firefox/packaging/install.rdf
new file mode 100644 (file)
index 0000000..8767666
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!-- 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/. --><RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>gssweb@painless-security.com</em:id>
+    <em:version>1.0</em:version>
+    <em:type>2</em:type>
+    <em:bootstrap>true</em:bootstrap>
+    <em:unpack>false</em:unpack>
+
+    <!-- Firefox -->
+    <em:targetApplication>
+      <Description>
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+        <em:minVersion>21.0</em:minVersion>
+      </Description>
+    </em:targetApplication>
+
+    <!-- Front End MetaData -->
+    <em:name>GSSWeb</em:name>
+    <em:description>GSS-EAP and GSSWeb package provider</em:description>
+    <em:creator>mark@painless-security.com</em:creator>
+    
+  </Description>
+</RDF>
diff --git a/browsers/firefox/packaging/locales.json b/browsers/firefox/packaging/locales.json
new file mode 100644 (file)
index 0000000..303e186
--- /dev/null
@@ -0,0 +1 @@
+{"locales": []}