diff options
Diffstat (limited to 'dom/media/IdpSandbox.jsm')
-rw-r--r-- | dom/media/IdpSandbox.jsm | 271 |
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; |