Labs/Jetpack/JEP/25
Problem Definition
It's currently difficult to share JavaScript code between different contexts in the Mozilla platform:
- Code written for the web, for instance, is difficult to load into a JS module, while JS modules and XPCOM components that don't use privileged functionality are impossible to load into web content without nontrivial modifications.
- JS modules and XPCOM components can't be unloaded and reloaded during the host application's runtime, which means that modifying a single line of their code requires a complete application restart.
- XPCOM components are extremely verbose and unfamiliar to those accustomed to traditional web development.
- JS modules have no concept of exporting chrome functionality to untrusted or semi-trusted code and vice versa.
- To further complicate matters, both XPCOM components and JS modules can't be easily reused in non-Mozilla contexts such as privileged server-side components like Helma and Persevere Server.
All of these problems are problems for Jetpack, and are barriers to a truly vibrant code-sharing ecosystem.
Our Solution
CommonJS is a grassroots group of JavaScript enthusiasts and professionals who are creating a common set of standards to make code easier to reuse. One such standard is SecurableModule, which provides a simple yet elegant module mechanism that works in any kind of context, including the web.
If you're not familiar with the SecurableModule standard, please do so now by following the aforementioned link; it's an extremely short specification.
Chrome Boosters are a superset of the SecurableModule standard that solve some issues specific to the Mozilla platform, and are intended to be used from both Jetpack Features and XULRunner extensions (e.g., Firefox/Thunderbird add-ons). Our hope is that, aside from solving the aforementioned problems, this module system will enable code-sharing between Jetpack developers, Add-on developers, web developers, and the greater JavaScript community as a whole.
As their name implies, Chrome Boosters are chrome-privileged and can use dependency injection to offer functionality to non-chrome-privileged Jetpacks—or, if used in a XULRunner extension, they can be used to provide functionality to other chrome-privileged code.
Once we have a substrate of Chrome Boosters available, we'll be able to create "Content Boosters" that consist solely of unprivileged functionality that itself relies on capabilities provided to them via Chrome Boosters.
These modules essentially formalize the mechanism used within Jetpack to provide the jetpack namespace to Jetpacks.
Chrome Boosters have the following characteristics:
- They are a superset of the CommonJS SecurableModule standard.
- They can be loaded and unloaded multiple times throughout the lifetime of their containing application (yes, leaks are an unfortunate possibility).
- They have the ability to register callbacks that perform cleanup tasks when unloaded.
- They have full access to XPConnect's Components object, and all its sub-objects.
When used in a XULRunner extension, the Chrome Booster framework should have the following additional characteristics:
- Adding Chrome Booster functionality to an existing XULRunner extension should be as painless as possible; ideally, it should consist merely of adding a single script, booster.js, and any desired SecurableModule files.
- Using booster.js should be just as easy in both a chrome-privileged document and a JS module (not to be confused with a SecurableModule).
For security purposes, Chrome Boosters can only be accessed either
- locally, through a containing addon, or
- over secure HTTP to a trusted host.
Additionally, to encourage good coding practices and documentation, Chrome Boosters must have:
- A simple, straightforward mechanism for writing and executing test cases.
- An easy-to-learn documentation system capable of producing beautiful documentation.
- A mechanism for tracking objects of interest belonging to the Chrome Booster to aid in memory profiling and leak detection.
Code Examples
This code doesn't necessarily work; it's just aspirational.
Jetpack Feature
Using Chrome Boosters from a Jetpack Feature is just like using them in any CommonJS-supported environment; the Jetpack runtime takes care of initializing the Chrome Booster framework when a Jetpack Feature is loaded, providing all necessary global functions to the Feature's global scope, and unloading the Chrome Boosters when the Feature is unloaded.
var foo = require('foo');
jetpack.notifications.show("Hello " + foo.bar());
Chrome Booster, exporting to chrome-privileged code
Creating a chrome booster that exports its functionality to chrome-privileged code follows the CommonJS standard for SecurableModules.
exports.bar = function bar() {
return "hello there";
}
XULRunner extension, in a chrome-privileged document
Assume that the following code is contained in a file at chrome://some_extension/content/foo.html.
<html>
<!-- This script makes the SecurableModule global available. -->
<script src="booster.js"></script>
<script>
// Create a loader for SecurableModules that gives them chrome
// privileges by default and roots the module path for
// require() calls to the local filesystem at
// chrome://some_extension/content/lib/.
var loader = SecurableModule.Loader({defaultPrincipal: "system",
rootPath: "lib/"});
// Load the 'foo' SecurableModule and call its exported doSomething()
// function.
loader.require('blarg').doSomething();
window.addEventListener(
"unload",
function() {
// Send an unload signal to free any resources created by modules so
// far.
loader.require('unload').send();
},
false
);
</script>
</html>
Note that the unload module conventions are laid out by the Narwhal CommonJS-based platform.
XULRunner extension, in a JS module
Chrome Booster functionality should be just as easy to access if the calling code is in a JS module instead of a page. As such, booster.js is engineered to detect which context it's loaded in and "just work" accordingly:
var SecurableModule = {};
Components.utils.import("resource://some_extension/content/booster.js",
SecurableModule);
var loader = SecurableModule.Loader({
defaultPrincipal: "system",
rootPath: "chrome://some_extension/content/lib/"
});
loader.require('blarg').doSomething();
Note that unlike the previous example, we can't provide a relative directory name for rootPath because JS modules don't conventionally have a concept of relative directories (and we won't introduce such a concept here to reduce potential confusion).
Note also that while we could unload the module if we want, we don't really have any pressing reason to, since JS modules themselves are never unloaded.
Chrome Booster, exporting to a Jetpack Feature
A Chrome Booster can use the forthcoming XPConnect Chrome Object Wrapper protocol to decide which properties it wants to export to untrusted or semi-trusted Jetpack code.
exports.foo = {
__exposedProps__ = {bar: 'r'},
bar: function bar() { /* ... */ },
baz: 5
};
In the above case, foo.bar() will be readable (but not writable) from Jetpacks that import the above module via a call to require(), but foo.baz will not be accessible at all.
Chrome Booster, exporting to a Jetpack Feature with capabilities
If a Jetpack Feature has capabilities associated with it, a Chrome Booster should be able to introspect into them and provide attenuated functionality based on said capabilities:
var caps = require('capabilities');
exports.foo = {
__exposedProps__ = {bar: 'r'},
bar: function bar() {
if (caps.has('file:read')) {
var fileObj = getSomeFile();
if (caps.has('file:write')) {
return fileObj;
}
return fileObj.makeReadOnly();
}
throw new SecurityError('Permission denied.');
},
baz: 5
};
Exactly how capabilities are specified for a Feature—e.g., whether done explicitly by Feature authors or implicitly via static analysis—is outside the scope of this document.
Exactly what capabilities are available in the Jetpack platform is also outside of the scope of this document, suffice to say that they will likely be developed organically: since the community will be creating Chrome Boosters that expose chrome functionality to semi-trusted content, we'll need to work together to figure out what the best capabilities are as the library of functionality is developed.
XULRunner extension, sandboxing code
It's possible to use the Chrome Booster framework API to load semi-trusted code the same way that Jetpack does. While this probably won't be needed in most cases for extensions, it's still a potentially nice way to sandbox an extension to follow the principle of least privilege. It's conceivable that this could also lead to quicker AMO reviews: if the vast majority of an extension runs in a sandbox and uses trusted, secure Chrome Boosters, then it should be easier for a reviewer to verify that the extension isn't insecure.
var SecurableModule = {};
Components.utils.import("resource://some_extension/content/booster.js",
SecurableModule);
var loader = SecurableModule.Loader({
rootPath: "chrome://some_extension/content/lib/"
});
loader.runScript({content: "require('foo').bar();",
capabilities: ["file:read"]});
Reference Implementation
An in-progress reference implementation for Chrome Boosters can be found at:
http://hg.mozilla.org/users/avarma_mozilla.com/jep-25/
Just check out the Mercurial repository and read the README.