summaryrefslogtreecommitdiffstats
path: root/dom/media/IdpSandbox.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/IdpSandbox.jsm')
-rw-r--r--dom/media/IdpSandbox.jsm271
1 files changed, 271 insertions, 0 deletions
diff --git a/dom/media/IdpSandbox.jsm b/dom/media/IdpSandbox.jsm
new file mode 100644
index 000000000..ada7efed7
--- /dev/null
+++ b/dom/media/IdpSandbox.jsm
@@ -0,0 +1,271 @@
+/* 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/. */
+
+'use strict';
+
+const {
+ classes: Cc,
+ interfaces: Ci,
+ utils: Cu,
+ results: Cr
+} = Components;
+
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/NetUtil.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+/** This little class ensures that redirects maintain an https:// origin */
+function RedirectHttpsOnly() {}
+
+RedirectHttpsOnly.prototype = {
+ asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
+ if (newChannel.URI.scheme !== 'https') {
+ callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
+ } else {
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ }
+ },
+
+ getInterface: function(iid) {
+ return this.QueryInterface(iid);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink])
+};
+
+/** This class loads a resource into a single string. ResourceLoader.load() is
+ * the entry point. */
+function ResourceLoader(res, rej) {
+ this.resolve = res;
+ this.reject = rej;
+ this.data = '';
+}
+
+/** Loads the identified https:// URL. */
+ResourceLoader.load = function(uri, doc) {
+ return new Promise((resolve, reject) => {
+ let listener = new ResourceLoader(resolve, reject);
+ let ioChannel = NetUtil.newChannel({
+ uri: uri,
+ loadingNode: doc,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_SCRIPT
+ });
+
+ ioChannel.loadGroup = doc.documentLoadGroup.QueryInterface(Ci.nsILoadGroup);
+ ioChannel.notificationCallbacks = new RedirectHttpsOnly();
+ ioChannel.asyncOpen2(listener);
+ });
+};
+
+ResourceLoader.prototype = {
+ onDataAvailable: function(request, context, input, offset, count) {
+ let stream = Cc['@mozilla.org/scriptableinputstream;1']
+ .createInstance(Ci.nsIScriptableInputStream);
+ stream.init(input);
+ this.data += stream.read(count);
+ },
+
+ onStartRequest: function (request, context) {},
+
+ onStopRequest: function(request, context, status) {
+ if (Components.isSuccessCode(status)) {
+ var statusCode = request.QueryInterface(Ci.nsIHttpChannel).responseStatus;
+ if (statusCode === 200) {
+ this.resolve({ request: request, data: this.data });
+ } else {
+ this.reject(new Error('Non-200 response from server: ' + statusCode));
+ }
+ } else {
+ this.reject(new Error('Load failed: ' + status));
+ }
+ },
+
+ getInterface: function(iid) {
+ return this.QueryInterface(iid);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener])
+};
+
+/**
+ * A simple implementation of the WorkerLocation interface.
+ */
+function createLocationFromURI(uri) {
+ return {
+ href: uri.spec,
+ protocol: uri.scheme + ':',
+ host: uri.host + ((uri.port >= 0) ?
+ (':' + uri.port) : ''),
+ port: uri.port,
+ hostname: uri.host,
+ pathname: uri.path.replace(/[#\?].*/, ''),
+ search: uri.path.replace(/^[^\?]*/, '').replace(/#.*/, ''),
+ hash: uri.hasRef ? ('#' + uri.ref) : '',
+ origin: uri.prePath,
+ toString: function() {
+ return uri.spec;
+ }
+ };
+}
+
+/**
+ * A javascript sandbox for running an IdP.
+ *
+ * @param domain (string) the domain of the IdP
+ * @param protocol (string?) the protocol of the IdP [default: 'default']
+ * @param win (obj) the current window
+ * @throws if the domain or protocol aren't valid
+ */
+function IdpSandbox(domain, protocol, win) {
+ this.source = IdpSandbox.createIdpUri(domain, protocol || "default");
+ this.active = null;
+ this.sandbox = null;
+ this.window = win;
+}
+
+IdpSandbox.checkDomain = function(domain) {
+ if (!domain || typeof domain !== 'string') {
+ throw new Error('Invalid domain for identity provider: ' +
+ 'must be a non-zero length string');
+ }
+};
+
+/**
+ * Checks that the IdP protocol is superficially sane. In particular, we don't
+ * want someone adding relative paths (e.g., '../../myuri'), which could be used
+ * to move outside of /.well-known/ and into space that they control.
+ */
+IdpSandbox.checkProtocol = function(protocol) {
+ let message = 'Invalid protocol for identity provider: ';
+ if (!protocol || typeof protocol !== 'string') {
+ throw new Error(message + 'must be a non-zero length string');
+ }
+ if (decodeURIComponent(protocol).match(/[\/\\]/)) {
+ throw new Error(message + "must not include '/' or '\\'");
+ }
+};
+
+/**
+ * Turns a domain and protocol into a URI. This does some aggressive checking
+ * to make sure that we aren't being fooled somehow. Throws on fooling.
+ */
+IdpSandbox.createIdpUri = function(domain, protocol) {
+ IdpSandbox.checkDomain(domain);
+ IdpSandbox.checkProtocol(protocol);
+
+ let message = 'Invalid IdP parameters: ';
+ try {
+ let wkIdp = 'https://' + domain + '/.well-known/idp-proxy/' + protocol;
+ let ioService = Components.classes['@mozilla.org/network/io-service;1']
+ .getService(Ci.nsIIOService);
+ let uri = ioService.newURI(wkIdp, null, null);
+
+ if (uri.hostPort !== domain) {
+ throw new Error(message + 'domain is invalid');
+ }
+ if (uri.path.indexOf('/.well-known/idp-proxy/') !== 0) {
+ throw new Error(message + 'must produce a /.well-known/idp-proxy/ URI');
+ }
+
+ return uri;
+ } catch (e if (typeof e.result !== 'undefined' &&
+ e.result === Cr.NS_ERROR_MALFORMED_URI)) {
+ throw new Error(message + 'must produce a valid URI');
+ }
+};
+
+IdpSandbox.prototype = {
+ isSame: function(domain, protocol) {
+ return this.source.spec === IdpSandbox.createIdpUri(domain, protocol).spec;
+ },
+
+ start: function() {
+ if (!this.active) {
+ this.active = ResourceLoader.load(this.source, this.window.document)
+ .then(result => this._createSandbox(result));
+ }
+ return this.active;
+ },
+
+ // Provides the sandbox with some useful facilities. Initially, this is only
+ // a minimal set; it is far easier to add more as the need arises, than to
+ // take them back if we discover a mistake.
+ _populateSandbox: function(uri) {
+ this.sandbox.location = Cu.cloneInto(createLocationFromURI(uri),
+ this.sandbox,
+ { cloneFunctions: true });
+ },
+
+ _createSandbox: function(result) {
+ let principal = Services.scriptSecurityManager
+ .getChannelResultPrincipal(result.request);
+
+ this.sandbox = Cu.Sandbox(principal, {
+ sandboxName: 'IdP-' + this.source.host,
+ wantComponents: false,
+ wantExportHelpers: false,
+ wantGlobalProperties: [
+ 'indexedDB', 'XMLHttpRequest', 'TextEncoder', 'TextDecoder',
+ 'URL', 'URLSearchParams', 'atob', 'btoa', 'Blob', 'crypto',
+ 'rtcIdentityProvider', 'fetch'
+ ]
+ });
+ let registrar = this.sandbox.rtcIdentityProvider;
+ if (!Cu.isXrayWrapper(registrar)) {
+ throw new Error('IdP setup failed');
+ }
+
+ // have to use the ultimate URI, not the starting one to avoid
+ // that origin stealing from the one that redirected to it
+ this._populateSandbox(result.request.URI);
+ try {
+ Cu.evalInSandbox(result.data, this.sandbox,
+ 'latest', result.request.URI.spec, 1);
+ } catch (e) {
+ // These can be passed straight on, because they are explicitly labelled
+ // as being IdP errors by the IdP and we drop line numbers as a result.
+ if (e.name === 'IdpError' || e.name === 'IdpLoginError') {
+ throw e;
+ }
+ this._logError(e);
+ throw new Error('Error in IdP, check console for details');
+ }
+
+ if (!registrar.hasIdp) {
+ throw new Error('IdP failed to call rtcIdentityProvider.register()');
+ }
+ return registrar;
+ },
+
+ // Capture all the details from the error and log them to the console. This
+ // can't rethrow anything else because that could leak information about the
+ // internal workings of the IdP across origins.
+ _logError: function(e) {
+ let winID = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+ let scriptError = Cc["@mozilla.org/scripterror;1"]
+ .createInstance(Ci.nsIScriptError);
+ scriptError.initWithWindowID(e.message, e.fileName, null,
+ e.lineNumber, e.columnNumber,
+ Ci.nsIScriptError.errorFlag,
+ "content javascript", winID);
+ let consoleService = Cc['@mozilla.org/consoleservice;1']
+ .getService(Ci.nsIConsoleService);
+ consoleService.logMessage(scriptError);
+ },
+
+ stop: function() {
+ if (this.sandbox) {
+ Cu.nukeSandbox(this.sandbox);
+ }
+ this.sandbox = null;
+ this.active = null;
+ },
+
+ toString: function() {
+ return this.source.spec;
+ }
+};
+
+this.EXPORTED_SYMBOLS = ['IdpSandbox'];
+this.IdpSandbox = IdpSandbox;