diff options
Diffstat (limited to 'dom/media/IdpSandbox.sys.mjs')
-rw-r--r-- | dom/media/IdpSandbox.sys.mjs | 284 |
1 files changed, 284 insertions, 0 deletions
diff --git a/dom/media/IdpSandbox.sys.mjs b/dom/media/IdpSandbox.sys.mjs new file mode 100644 index 0000000000..bd864b2fab --- /dev/null +++ b/dom/media/IdpSandbox.sys.mjs @@ -0,0 +1,284 @@ +/* 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/. */ + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +/** This little class ensures that redirects maintain an https:// origin */ +function RedirectHttpsOnly() {} + +RedirectHttpsOnly.prototype = { + asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { + if (newChannel.URI.scheme !== "https") { + callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT); + } else { + callback.onRedirectVerifyCallback(Cr.NS_OK); + } + }, + + getInterface(iid) { + return this.QueryInterface(iid); + }, + QueryInterface: ChromeUtils.generateQI(["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, + loadingNode: doc, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_SCRIPT, + }); + + ioChannel.loadGroup = doc.documentLoadGroup.QueryInterface(Ci.nsILoadGroup); + ioChannel.notificationCallbacks = new RedirectHttpsOnly(); + ioChannel.asyncOpen(listener); + }); +}; + +ResourceLoader.prototype = { + onDataAvailable(request, input, offset, count) { + let stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + stream.init(input); + this.data += stream.read(count); + }, + + onStartRequest(request) {}, + + onStopRequest(request, status) { + if (Components.isSuccessCode(status)) { + var statusCode = request.QueryInterface(Ci.nsIHttpChannel).responseStatus; + if (statusCode === 200) { + this.resolve({ request, data: this.data }); + } else { + this.reject(new Error("Non-200 response from server: " + statusCode)); + } + } else { + this.reject(new Error("Load failed: " + status)); + } + }, + + getInterface(iid) { + return this.QueryInterface(iid); + }, + QueryInterface: ChromeUtils.generateQI(["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.pathQueryRef.replace(/[#\?].*/, ""), + search: uri.pathQueryRef.replace(/^[^\?]*/, "").replace(/#.*/, ""), + hash: uri.hasRef ? "#" + uri.ref : "", + origin: uri.prePath, + toString() { + 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 + */ +export 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 uri = Services.io.newURI(wkIdp); + + if (uri.hostPort !== domain) { + throw new Error(message + "domain is invalid"); + } + if (uri.pathQueryRef.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"); + } + throw e; + } +}; + +IdpSandbox.prototype = { + isSame(domain, protocol) { + return this.source.spec === IdpSandbox.createIdpUri(domain, protocol).spec; + }, + + start() { + 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(uri) { + this.sandbox.location = Cu.cloneInto( + createLocationFromURI(uri), + this.sandbox, + { cloneFunctions: true } + ); + }, + + _createSandbox(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(e) { + let winID = this.window.windowGlobalChild.innerWindowId; + 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 + ); + Services.console.logMessage(scriptError); + }, + + stop() { + if (this.sandbox) { + Cu.nukeSandbox(this.sandbox); + } + this.sandbox = null; + this.active = null; + }, + + toString() { + return this.source.spec; + }, +}; |