mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 975144 - Rework RTC identity to use JS sandbox, r=jib
This commit is contained in:
parent
63540e94d0
commit
c24d8aeadd
@ -1,272 +0,0 @@
|
||||
/* 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";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["IdpProxy"];
|
||||
|
||||
const {
|
||||
classes: Cc,
|
||||
interfaces: Ci,
|
||||
utils: Cu,
|
||||
results: Cr
|
||||
} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Sandbox",
|
||||
"resource://gre/modules/identity/Sandbox.jsm");
|
||||
|
||||
/**
|
||||
* An invisible iframe for hosting the idp shim.
|
||||
*
|
||||
* There is no visible UX here, as we assume the user has already
|
||||
* logged in elsewhere (on a different screen in the web site hosting
|
||||
* the RTC functions).
|
||||
*/
|
||||
function IdpChannel(uri, messageCallback) {
|
||||
this.sandbox = null;
|
||||
this.messagechannel = null;
|
||||
this.source = uri;
|
||||
this.messageCallback = messageCallback;
|
||||
}
|
||||
|
||||
IdpChannel.prototype = {
|
||||
/**
|
||||
* Create a hidden, sandboxed iframe for hosting the IdP's js shim.
|
||||
*
|
||||
* @param callback
|
||||
* (function) invoked when this completes, with an error
|
||||
* argument if there is a problem, no argument if everything is
|
||||
* ok
|
||||
*/
|
||||
open: function(callback) {
|
||||
if (this.sandbox) {
|
||||
return callback(new Error("IdP channel already open"));
|
||||
}
|
||||
|
||||
let ready = this._sandboxReady.bind(this, callback);
|
||||
this.sandbox = new Sandbox(this.source, ready);
|
||||
},
|
||||
|
||||
_sandboxReady: function(aCallback, aSandbox) {
|
||||
// Inject a message channel into the subframe.
|
||||
try {
|
||||
this.messagechannel = new aSandbox._frame.contentWindow.MessageChannel();
|
||||
Object.defineProperty(
|
||||
aSandbox._frame.contentWindow.wrappedJSObject,
|
||||
"rtcwebIdentityPort",
|
||||
{
|
||||
value: this.messagechannel.port2,
|
||||
configurable: true
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
this.close();
|
||||
aCallback(e); // oops, the IdP proxy overwrote this.. bad
|
||||
return;
|
||||
}
|
||||
this.messagechannel.port1.onmessage = function(msg) {
|
||||
this.messageCallback(msg.data);
|
||||
}.bind(this);
|
||||
this.messagechannel.port1.start();
|
||||
aCallback();
|
||||
},
|
||||
|
||||
send: function(msg) {
|
||||
this.messagechannel.port1.postMessage(msg);
|
||||
},
|
||||
|
||||
close: function IdpChannel_close() {
|
||||
if (this.sandbox) {
|
||||
if (this.messagechannel) {
|
||||
this.messagechannel.port1.close();
|
||||
}
|
||||
this.sandbox.free();
|
||||
}
|
||||
this.messagechannel = null;
|
||||
this.sandbox = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A message channel between the RTC PeerConnection and a designated IdP Proxy.
|
||||
*
|
||||
* @param domain (string) the domain to load up
|
||||
* @param protocol (string) Optional string for the IdP protocol
|
||||
*/
|
||||
function IdpProxy(domain, protocol) {
|
||||
IdpProxy.validateDomain(domain);
|
||||
IdpProxy.validateProtocol(protocol);
|
||||
|
||||
this.domain = domain;
|
||||
this.protocol = protocol || "default";
|
||||
|
||||
this._reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the domain is only a domain, and doesn't contain anything else.
|
||||
* Adds it to a URI, then checks that it matches perfectly.
|
||||
*/
|
||||
IdpProxy.validateDomain = function(domain) {
|
||||
let message = "Invalid domain for identity provider; ";
|
||||
if (!domain || typeof domain !== "string") {
|
||||
throw new Error(message + "must be a non-zero length string");
|
||||
}
|
||||
|
||||
message += "must only have a domain name and optionally a port";
|
||||
try {
|
||||
let ioService = Components.classes["@mozilla.org/network/io-service;1"]
|
||||
.getService(Components.interfaces.nsIIOService);
|
||||
let uri = ioService.newURI('https://' + domain + '/', null, null);
|
||||
|
||||
// this should trap errors
|
||||
// we could check uri.userPass, uri.path and uri.ref, but there is no need
|
||||
if (uri.hostPort !== domain) {
|
||||
throw new Error(message);
|
||||
}
|
||||
} catch (e if (e.result === Cr.NS_ERROR_MALFORMED_URI)) {
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks that the IdP protocol is 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.
|
||||
*/
|
||||
IdpProxy.validateProtocol = function(protocol) {
|
||||
if (!protocol) {
|
||||
return; // falsy values turn into "default", so they are OK
|
||||
}
|
||||
let message = "Invalid protocol for identity provider; ";
|
||||
if (typeof protocol !== "string") {
|
||||
throw new Error(message + "must be a string");
|
||||
}
|
||||
if (decodeURIComponent(protocol).match(/[\/\\]/)) {
|
||||
throw new Error(message + "must not include '/' or '\\'");
|
||||
}
|
||||
};
|
||||
|
||||
IdpProxy.prototype = {
|
||||
_reset: function() {
|
||||
this.channel = null;
|
||||
this.ready = false;
|
||||
|
||||
this.counter = 0;
|
||||
this.tracking = {};
|
||||
this.pending = [];
|
||||
},
|
||||
|
||||
isSame: function(domain, protocol) {
|
||||
return this.domain === domain && ((protocol || "default") === this.protocol);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a sandboxed iframe for hosting the idp-proxy's js. Create a message
|
||||
* channel down to the frame.
|
||||
*
|
||||
* @param errorCallback (function) a callback that will be invoked if there
|
||||
* is a fatal error starting the proxy
|
||||
*/
|
||||
start: function(errorCallback) {
|
||||
if (this.channel) {
|
||||
return;
|
||||
}
|
||||
let well_known = "https://" + this.domain;
|
||||
well_known += "/.well-known/idp-proxy/" + this.protocol;
|
||||
this.channel = new IdpChannel(well_known, this._messageReceived.bind(this));
|
||||
this.channel.open(function(error) {
|
||||
if (error) {
|
||||
this.close();
|
||||
if (typeof errorCallback === "function") {
|
||||
errorCallback(error);
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a message up to the idp proxy. This should be an RTC "SIGN" or
|
||||
* "VERIFY" message. This method adds the tracking 'id' parameter
|
||||
* automatically to the message so that the callback is only invoked for the
|
||||
* response to the message.
|
||||
*
|
||||
* This enqueues the message to send if the IdP hasn't signaled that it is
|
||||
* "READY", and sends the message when it is.
|
||||
*
|
||||
* The caller is responsible for ensuring that a response is received. If the
|
||||
* IdP doesn't respond, the callback simply isn't invoked.
|
||||
*/
|
||||
send: function(message, callback) {
|
||||
this.start();
|
||||
if (this.ready) {
|
||||
message.id = "" + (++this.counter);
|
||||
this.tracking[message.id] = callback;
|
||||
this.channel.send(message);
|
||||
} else {
|
||||
this.pending.push({ message: message, callback: callback });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a message from the IdP. This automatically sends if the message is
|
||||
* 'READY' so there is no need to track readiness state outside of this obj.
|
||||
*/
|
||||
_messageReceived: function(message) {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
if (!this.ready && message.type === "READY") {
|
||||
this.ready = true;
|
||||
this.pending.forEach(function(p) {
|
||||
this.send(p.message, p.callback);
|
||||
}, this);
|
||||
this.pending = [];
|
||||
} else if (this.tracking[message.id]) {
|
||||
var callback = this.tracking[message.id];
|
||||
delete this.tracking[message.id];
|
||||
callback(message);
|
||||
} else {
|
||||
let console = Cc["@mozilla.org/consoleservice;1"].
|
||||
getService(Ci.nsIConsoleService);
|
||||
console.logStringMessage("Received bad message from IdP: " +
|
||||
message.id + ":" + message.type);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs cleanup. The object should be OK to use again.
|
||||
*/
|
||||
close: function() {
|
||||
if (!this.channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// clear out before letting others know in case they do something bad
|
||||
let trackingCopy = this.tracking;
|
||||
let pendingCopy = this.pending;
|
||||
|
||||
this.channel.close();
|
||||
this._reset();
|
||||
|
||||
// dump a message of type "ERROR" in response to all outstanding
|
||||
// messages to the IdP
|
||||
let error = { type: "ERROR", error: "IdP closed" };
|
||||
Object.keys(trackingCopy).forEach(function(k) {
|
||||
trackingCopy[k](error);
|
||||
});
|
||||
pendingCopy.forEach(function(p) {
|
||||
p.callback(error);
|
||||
});
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return this.domain + '/.../' + this.protocol;
|
||||
}
|
||||
};
|
||||
|
||||
this.IdpProxy = IdpProxy;
|
238
dom/media/IdpSandbox.jsm
Normal file
238
dom/media/IdpSandbox.jsm
Normal file
@ -0,0 +1,238 @@
|
||||
/* 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/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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let listener = new ResourceLoader(resolve, reject);
|
||||
let ioService = Cc['@mozilla.org/network/io-service;1']
|
||||
.getService(Ci.nsIIOService);
|
||||
let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
|
||||
// the '2' identifies this as a script load
|
||||
let ioChannel = ioService.newChannelFromURI2(uri, null, systemPrincipal,
|
||||
systemPrincipal, 0, 2);
|
||||
ioChannel.notificationCallbacks = new RedirectHttpsOnly();
|
||||
ioChannel.asyncOpen(listener, null);
|
||||
});
|
||||
};
|
||||
|
||||
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']
|
||||
* @throws if the domain or protocol aren't valid
|
||||
*/
|
||||
function IdpSandbox(domain, protocol) {
|
||||
this.source = IdpSandbox.createIdpUri(domain, protocol || "default");
|
||||
this.active = null;
|
||||
this.sandbox = null;
|
||||
}
|
||||
|
||||
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)
|
||||
.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() {
|
||||
this.sandbox.location = Cu.cloneInto(createLocationFromURI(this.source),
|
||||
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'
|
||||
]
|
||||
});
|
||||
this._populateSandbox();
|
||||
|
||||
let registrar = this.sandbox.rtcIdentityProvider;
|
||||
if (!Cu.isXrayWrapper(registrar)) {
|
||||
throw new Error('IdP setup failed');
|
||||
}
|
||||
// putting a javascript version of 1.8 here seems fragile
|
||||
Cu.evalInSandbox(result.data, this.sandbox,
|
||||
'1.8', result.request.URI.spec, 1);
|
||||
|
||||
if (!registrar.idp) {
|
||||
throw new Error('IdP failed to call rtcIdentityProvider.register()');
|
||||
}
|
||||
return registrar;
|
||||
},
|
||||
|
||||
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;
|
@ -389,11 +389,13 @@ RTCPeerConnection.prototype = {
|
||||
_initIdp: function() {
|
||||
let prefName = "media.peerconnection.identity.timeout";
|
||||
let idpTimeout = Services.prefs.getIntPref(prefName);
|
||||
let warningFunc = this.logWarning.bind(this);
|
||||
this._localIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
|
||||
this.dispatchEvent.bind(this));
|
||||
this._remoteIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
|
||||
this.dispatchEvent.bind(this));
|
||||
let warn = this.logWarning.bind(this);
|
||||
let idpErrorReport = (type, args) => {
|
||||
this.dispatchEvent(
|
||||
new this._win.RTCPeerConnectionIdentityErrorEvent(type, args));
|
||||
};
|
||||
this._localIdp = new PeerConnectionIdp(idpTimeout, warn, idpErrorReport);
|
||||
this._remoteIdp = new PeerConnectionIdp(idpTimeout, warn, idpErrorReport);
|
||||
},
|
||||
|
||||
// Add a function to the internal operations chain.
|
||||
@ -706,23 +708,22 @@ RTCPeerConnection.prototype = {
|
||||
this._impl.setRemoteDescription(type, desc.sdp);
|
||||
});
|
||||
|
||||
let pp = new Promise(resolve =>
|
||||
this._remoteIdp.verifyIdentityFromSDP(desc.sdp, origin, resolve))
|
||||
.then(msg => {
|
||||
// If this pc has an identity already, then identity in sdp must match
|
||||
if (expectedIdentity && (!msg || msg.identity !== expectedIdentity)) {
|
||||
throw new this._win.DOMException(
|
||||
"Peer Identity mismatch, expected: " + expectedIdentity,
|
||||
"IncompatibleSessionDescriptionError");
|
||||
}
|
||||
if (msg) {
|
||||
// Set new identity and generate an event.
|
||||
this._impl.peerIdentity = msg.identity;
|
||||
this._peerIdentity = new this._win.RTCIdentityAssertion(
|
||||
this._remoteIdp.provider, msg.identity);
|
||||
this.dispatchEvent(new this._win.Event("peeridentity"));
|
||||
}
|
||||
});
|
||||
let pp = this._remoteIdp.verifyIdentityFromSDP(desc.sdp, origin)
|
||||
.then(msg => {
|
||||
// If this pc has an identity already, then identity in sdp must match
|
||||
if (expectedIdentity && (!msg || msg.identity !== expectedIdentity)) {
|
||||
throw new this._win.DOMException(
|
||||
"Peer Identity mismatch, expected: " + expectedIdentity,
|
||||
"IncompatibleSessionDescriptionError");
|
||||
}
|
||||
if (msg) {
|
||||
// Set new identity and generate an event.
|
||||
this._impl.peerIdentity = msg.identity;
|
||||
this._peerIdentity = new this._win.RTCIdentityAssertion(
|
||||
this._remoteIdp.provider, msg.identity);
|
||||
this.dispatchEvent(new this._win.Event("peeridentity"));
|
||||
}
|
||||
});
|
||||
// Only wait for Idp validation if we need identity matching.
|
||||
return expectedIdentity? this._win.Promise.all([p, pp]).then(() => {}) : p;
|
||||
});
|
||||
@ -734,7 +735,10 @@ RTCPeerConnection.prototype = {
|
||||
this._localIdp.setIdentityProvider(provider, protocol, username);
|
||||
},
|
||||
|
||||
_gotIdentityAssertion: function(assertion){
|
||||
_gotIdentityAssertion: function(assertion) {
|
||||
if (!assertion) {
|
||||
return;
|
||||
}
|
||||
let args = { assertion: assertion };
|
||||
let ev = new this._win.RTCPeerConnectionIdentityEvent("identityresult", args);
|
||||
this.dispatchEvent(ev);
|
||||
@ -743,14 +747,8 @@ RTCPeerConnection.prototype = {
|
||||
getIdentityAssertion: function() {
|
||||
this._checkClosed();
|
||||
|
||||
var gotAssertion = assertion => {
|
||||
if (assertion) {
|
||||
this._gotIdentityAssertion(assertion);
|
||||
}
|
||||
};
|
||||
|
||||
this._localIdp.getIdentityAssertion(this._impl.fingerprint,
|
||||
gotAssertion);
|
||||
this._localIdp.getIdentityAssertion(this._impl.fingerprint)
|
||||
.then(assertion => this._gotIdentityAssertion(assertion));
|
||||
},
|
||||
|
||||
updateIce: function(config) {
|
||||
@ -888,7 +886,7 @@ RTCPeerConnection.prototype = {
|
||||
return null;
|
||||
}
|
||||
|
||||
sdp = this._localIdp.wrapSdp(sdp);
|
||||
sdp = this._localIdp.addIdentityAttribute(sdp);
|
||||
return new this._win.mozRTCSessionDescription({ type: this._localType,
|
||||
sdp: sdp });
|
||||
},
|
||||
@ -1042,15 +1040,20 @@ PeerConnectionObserver.prototype = {
|
||||
|
||||
onCreateOfferSuccess: function(sdp) {
|
||||
let pc = this._dompc;
|
||||
let fp = pc._impl.fingerprint;
|
||||
let origin = Cu.getWebIDLCallerPrincipal().origin;
|
||||
pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
|
||||
if (assertion) {
|
||||
pc._gotIdentityAssertion(assertion);
|
||||
}
|
||||
let idp = pc._localIdp;
|
||||
|
||||
if (idp.enabled) {
|
||||
idp.getIdentityAssertion(pc._impl.fingerprint)
|
||||
.then(assertion => {
|
||||
pc._gotIdentityAssertion(assertion);
|
||||
sdp = idp.addIdentityAttribute(sdp);
|
||||
pc._onCreateOfferSuccess(new pc._win.mozRTCSessionDescription({ type: "offer",
|
||||
sdp: sdp }));
|
||||
}, e => {}); // errors are handled in the IdP
|
||||
} else {
|
||||
pc._onCreateOfferSuccess(new pc._win.mozRTCSessionDescription({ type: "offer",
|
||||
sdp: sdp }));
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
onCreateOfferError: function(code, message) {
|
||||
@ -1059,15 +1062,20 @@ PeerConnectionObserver.prototype = {
|
||||
|
||||
onCreateAnswerSuccess: function(sdp) {
|
||||
let pc = this._dompc;
|
||||
let fp = pc._impl.fingerprint;
|
||||
let origin = Cu.getWebIDLCallerPrincipal().origin;
|
||||
pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
|
||||
if (assertion) {
|
||||
pc._gotIdentityAssertion(assertion);
|
||||
}
|
||||
let idp = pc._localIdp;
|
||||
|
||||
if (idp.enabled) {
|
||||
idp.getIdentityAssertion(pc._impl.fingerprint)
|
||||
.then(assertion => {
|
||||
pc._gotIdentityAssertion(assertion);
|
||||
sdp = idp.addIdentityAttribute(sdp);
|
||||
pc._onCreateAnswerSuccess(new pc._win.mozRTCSessionDescription({ type: "answer",
|
||||
sdp: sdp }));
|
||||
}, e => {});
|
||||
} else {
|
||||
pc._onCreateAnswerSuccess(new pc._win.mozRTCSessionDescription({ type: "answer",
|
||||
sdp: sdp }));
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
onCreateAnswerError: function(code, message) {
|
||||
|
@ -3,68 +3,93 @@
|
||||
* 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/. */
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["PeerConnectionIdp"];
|
||||
this.EXPORTED_SYMBOLS = ['PeerConnectionIdp'];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "IdpProxy",
|
||||
"resource://gre/modules/media/IdpProxy.jsm");
|
||||
Cu.import('resource://gre/modules/Services.jsm');
|
||||
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
|
||||
XPCOMUtils.defineLazyModuleGetter(this, 'IdpSandbox',
|
||||
'resource://gre/modules/media/IdpSandbox.jsm');
|
||||
|
||||
function TimerResolver(resolve) {
|
||||
this.notify = resolve;
|
||||
}
|
||||
TimerResolver.prototype = {
|
||||
getInterface: function(iid) {
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
|
||||
}
|
||||
function delay(t) {
|
||||
return new Promise(resolve => {
|
||||
let timer = Cc['@mozilla.org/timer;1'].getService(Ci.nsITimer);
|
||||
timer.initWithCallback(new TimerResolver(resolve), t, 0); // One shot
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an IdP helper.
|
||||
*
|
||||
* @param window (object) the window object to use for miscellaneous goodies
|
||||
* @param timeout (int) the timeout in milliseconds
|
||||
* @param warningFunc (function) somewhere to dump warning messages
|
||||
* @param dispatchEventFunc (function) somewhere to dump error events
|
||||
* @param dispatchErrorFunc (function(string, dict)) somewhere to dump errors
|
||||
*/
|
||||
function PeerConnectionIdp(window, timeout, warningFunc, dispatchEventFunc) {
|
||||
this._win = window;
|
||||
function PeerConnectionIdp(timeout, warningFunc, dispatchErrorFunc) {
|
||||
this._timeout = timeout || 5000;
|
||||
this._warning = warningFunc;
|
||||
this._dispatchEvent = dispatchEventFunc;
|
||||
this._dispatchError = dispatchErrorFunc;
|
||||
|
||||
this.assertion = null;
|
||||
this.provider = null;
|
||||
}
|
||||
|
||||
(function() {
|
||||
PeerConnectionIdp._mLinePattern = new RegExp("^m=", "m");
|
||||
PeerConnectionIdp._mLinePattern = new RegExp('^m=', 'm');
|
||||
// attributes are funny, the 'a' is case sensitive, the name isn't
|
||||
let pattern = "^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)";
|
||||
PeerConnectionIdp._identityPattern = new RegExp(pattern, "m");
|
||||
pattern = "^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)";
|
||||
PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, "m");
|
||||
let pattern = '^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)';
|
||||
PeerConnectionIdp._identityPattern = new RegExp(pattern, 'm');
|
||||
pattern = '^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)';
|
||||
PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, 'm');
|
||||
})();
|
||||
|
||||
PeerConnectionIdp.prototype = {
|
||||
get enabled() {
|
||||
return !!this._idp;
|
||||
},
|
||||
|
||||
setIdentityProvider: function(provider, protocol, username) {
|
||||
this.provider = provider;
|
||||
this.protocol = protocol;
|
||||
this.protocol = protocol || 'default';
|
||||
this.username = username;
|
||||
if (this._idpchannel) {
|
||||
if (this._idpchannel.isSame(provider, protocol)) {
|
||||
return;
|
||||
if (this._idp) {
|
||||
if (this._idp.isSame(provider, protocol)) {
|
||||
return; // noop
|
||||
}
|
||||
this._idpchannel.close();
|
||||
this._idp.stop();
|
||||
}
|
||||
this._idpchannel = new IdpProxy(provider, protocol);
|
||||
this._idp = new IdpSandbox(provider, protocol);
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this.assertion = null;
|
||||
this.provider = null;
|
||||
if (this._idpchannel) {
|
||||
this._idpchannel.close();
|
||||
this._idpchannel = null;
|
||||
this.protocol = null;
|
||||
if (this._idp) {
|
||||
this._idp.stop();
|
||||
this._idp = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate an error event of the identified type;
|
||||
* and put a little more precise information in the console.
|
||||
*
|
||||
* A little note on error handling in this class: this class reports errors
|
||||
* exclusively through the event handlers that are passed to it
|
||||
* (this._dispatchError, specifically). That means that all the functions
|
||||
* return resolved promises; promises are never rejected. This probably isn't
|
||||
* the best design, but the refactor can wait.
|
||||
*/
|
||||
reportError: function(type, message, extra) {
|
||||
let args = {
|
||||
@ -76,9 +101,8 @@ PeerConnectionIdp.prototype = {
|
||||
args[k] = extra[k];
|
||||
});
|
||||
}
|
||||
this._warning("RTC identity: " + message, null, 0);
|
||||
let ev = new this._win.RTCPeerConnectionIdentityErrorEvent('idp' + type + 'error', args);
|
||||
this._dispatchEvent(ev);
|
||||
this._warning('RTC identity: ' + message, null, 0);
|
||||
this._dispatchError('idp' + type + 'error', args);
|
||||
},
|
||||
|
||||
_getFingerprintsFromSdp: function(sdp) {
|
||||
@ -93,169 +117,163 @@ PeerConnectionIdp.prototype = {
|
||||
return Object.keys(fingerprints).map(k => fingerprints[k]);
|
||||
},
|
||||
|
||||
_isValidAssertion: function(assertion) {
|
||||
return assertion && assertion.idp &&
|
||||
typeof assertion.idp.domain === 'string' &&
|
||||
(!assertion.idp.protocol ||
|
||||
typeof assertion.idp.protocol === 'string') &&
|
||||
typeof assertion.assertion === 'string';
|
||||
},
|
||||
|
||||
_getIdentityFromSdp: function(sdp) {
|
||||
// a=identity is session level
|
||||
let idMatch;
|
||||
let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern);
|
||||
if (mLineMatch) {
|
||||
let sessionLevel = sdp.substring(0, mLineMatch.index);
|
||||
idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
|
||||
let idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
|
||||
}
|
||||
if (!idMatch) {
|
||||
return; // undefined === no identity
|
||||
}
|
||||
if (idMatch) {
|
||||
let assertion = {};
|
||||
try {
|
||||
assertion = JSON.parse(atob(idMatch[1]));
|
||||
} catch (e) {
|
||||
this.reportError("validation",
|
||||
"invalid identity assertion: " + e);
|
||||
} // for JSON.parse
|
||||
if (typeof assertion.idp === "object" &&
|
||||
typeof assertion.idp.domain === "string" &&
|
||||
typeof assertion.assertion === "string") {
|
||||
return assertion;
|
||||
}
|
||||
|
||||
this.reportError("validation", "assertion missing" +
|
||||
" idp/idp.domain/assertion");
|
||||
let assertion;
|
||||
try {
|
||||
assertion = JSON.parse(atob(idMatch[1]));
|
||||
} catch (e) {
|
||||
this.reportError('validation',
|
||||
'invalid identity assertion: ' + e);
|
||||
}
|
||||
// undefined!
|
||||
if (!this._isValidAssertion(assertion)) {
|
||||
this.reportError('validation', 'assertion missing' +
|
||||
' idp/idp.domain/assertion');
|
||||
}
|
||||
return assertion;
|
||||
},
|
||||
|
||||
/**
|
||||
* Queues a task to verify the a=identity line the given SDP contains, if any.
|
||||
* Verifies the a=identity line the given SDP contains, if any.
|
||||
* If the verification succeeds callback is called with the message from the
|
||||
* IdP proxy as parameter, else (verification failed OR no a=identity line in
|
||||
* SDP at all) null is passed to callback.
|
||||
*
|
||||
* Note that this only verifies that the SDP is coherent. This relies on the
|
||||
* invariant that the RTCPeerConnection won't connect to a peer if the
|
||||
* fingerprint of the certificate they offer doesn't appear in the SDP.
|
||||
*/
|
||||
verifyIdentityFromSDP: function(sdp, origin, callback) {
|
||||
verifyIdentityFromSDP: function(sdp, origin) {
|
||||
let identity = this._getIdentityFromSdp(sdp);
|
||||
let fingerprints = this._getFingerprintsFromSdp(sdp);
|
||||
// it's safe to use the fingerprint we got from the SDP here,
|
||||
// only because we ensure that there is only one
|
||||
if (!identity || fingerprints.length <= 0) {
|
||||
callback(null);
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
|
||||
this._verifyIdentity(identity.assertion, fingerprints, origin, callback);
|
||||
return this._verifyIdentity(identity.assertion, fingerprints, origin);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the name in the identity provided by the IdP is OK.
|
||||
*
|
||||
* @param error (function) an error function to call
|
||||
* @param name (string) the name to validate
|
||||
* @returns (string) an error message, iff the name isn't good
|
||||
* @throws if the name isn't valid
|
||||
*/
|
||||
_validateName: function(name) {
|
||||
if (typeof name !== "string") {
|
||||
return "name not a string";
|
||||
_validateName: function(error, name) {
|
||||
if (typeof name !== 'string') {
|
||||
return error('name not a string');
|
||||
}
|
||||
let atIdx = name.indexOf('@');
|
||||
if (atIdx <= 0) {
|
||||
return error('missing authority in name from IdP');
|
||||
}
|
||||
let atIdx = name.indexOf("@");
|
||||
if (atIdx > 0) {
|
||||
// no third party assertions... for now
|
||||
let tail = name.substring(atIdx + 1);
|
||||
|
||||
// strip the port number, if present
|
||||
let provider = this.provider;
|
||||
let providerPortIdx = provider.indexOf(":");
|
||||
if (providerPortIdx > 0) {
|
||||
provider = provider.substring(0, providerPortIdx);
|
||||
}
|
||||
let idnService = Components.classes["@mozilla.org/network/idn-service;1"].
|
||||
getService(Components.interfaces.nsIIDNService);
|
||||
if (idnService.convertUTF8toACE(tail) !==
|
||||
idnService.convertUTF8toACE(provider)) {
|
||||
return "name '" + identity.name +
|
||||
"' doesn't match IdP: '" + this.provider + "'";
|
||||
}
|
||||
return null;
|
||||
// no third party assertions... for now
|
||||
let tail = name.substring(atIdx + 1);
|
||||
|
||||
// strip the port number, if present
|
||||
let provider = this.provider;
|
||||
let providerPortIdx = provider.indexOf(':');
|
||||
if (providerPortIdx > 0) {
|
||||
provider = provider.substring(0, providerPortIdx);
|
||||
}
|
||||
return "missing authority in name from IdP";
|
||||
let idnService = Components.classes['@mozilla.org/network/idn-service;1'].
|
||||
getService(Components.interfaces.nsIIDNService);
|
||||
if (idnService.convertUTF8toACE(tail) !==
|
||||
idnService.convertUTF8toACE(provider)) {
|
||||
return error('name "' + identity.name +
|
||||
'" doesn\'t match IdP: "' + this.provider + '"');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// we are very defensive here when handling the message from the IdP
|
||||
// proxy so that broken IdPs can only do as little harm as possible.
|
||||
_checkVerifyResponse: function(message, fingerprints) {
|
||||
let warn = msg => {
|
||||
this.reportError("validation",
|
||||
"assertion validation failure: " + msg);
|
||||
/**
|
||||
* Check the validation response. We are very defensive here when handling
|
||||
* the message from the IdP proxy. That way, broken IdPs aren't likely to
|
||||
* cause catastrophic damage.
|
||||
*/
|
||||
_isValidVerificationResponse: function(validation, sdpFingerprints) {
|
||||
let error = msg => {
|
||||
this.reportError('validation', 'assertion validation failure: ' + msg);
|
||||
return false;
|
||||
};
|
||||
|
||||
let isSubsetOf = (outer, inner, cmp) => {
|
||||
return inner.some(i => {
|
||||
return !outer.some(o => cmp(i, o));
|
||||
if (typeof validation !== 'object' ||
|
||||
typeof validation.contents !== 'string' ||
|
||||
typeof validation.identity !== 'string') {
|
||||
return error('no payload in validation response');
|
||||
}
|
||||
|
||||
let fingerprints;
|
||||
try {
|
||||
fingerprints = JSON.parse(validation.contents).fingerprint;
|
||||
} catch (e) {
|
||||
return error('idp returned invalid JSON');
|
||||
}
|
||||
|
||||
let isFingerprint = f =>
|
||||
(typeof f.digest === 'string') &&
|
||||
(typeof f.algorithm === 'string');
|
||||
if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
|
||||
return error('fingerprints must be an array of objects' +
|
||||
' with digest and algorithm attributes');
|
||||
}
|
||||
|
||||
let isSubsetOf = (outerSet, innerSet, comparator) => {
|
||||
return innerSet.every(i => {
|
||||
return outerSet.some(o => comparator(i, o));
|
||||
});
|
||||
};
|
||||
let compareFingerprints = (a, b) => {
|
||||
return (a.digest === b.digest) && (a.algorithm === b.algorithm);
|
||||
};
|
||||
if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) {
|
||||
return error('the fingerprints in SDP aren\'t covered by the assertion');
|
||||
}
|
||||
return this._validateName(error, validation.identity);
|
||||
},
|
||||
|
||||
try {
|
||||
let contents = JSON.parse(message.contents);
|
||||
if (!Array.isArray(contents.fingerprint)) {
|
||||
warn("fingerprint is not an array");
|
||||
} else if (isSubsetOf(contents.fingerprint, fingerprints,
|
||||
compareFingerprints)) {
|
||||
warn("fingerprints in SDP aren't a subset of those in the assertion");
|
||||
} else {
|
||||
let error = this._validateName(message.identity);
|
||||
if (error) {
|
||||
warn(error);
|
||||
} else {
|
||||
return true;
|
||||
/**
|
||||
* Asks the IdP proxy to verify an identity assertion.
|
||||
*/
|
||||
_verifyIdentity: function(assertion, fingerprints, origin) {
|
||||
let validationPromise = this._idp.start()
|
||||
.then(idp => idp.validateAssertion(assertion, origin));
|
||||
|
||||
return this._safetyNet('validation', validationPromise)
|
||||
.then(validation => {
|
||||
if (validation &&
|
||||
this._isValidVerificationResponse(validation, fingerprints)) {
|
||||
return validation;
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
warn("invalid JSON in content");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Asks the IdP proxy to verify an identity.
|
||||
* Enriches the given SDP with an `a=identity` line. getIdentityAssertion()
|
||||
* must have already run successfully, otherwise this does nothing to the sdp.
|
||||
*/
|
||||
_verifyIdentity: function(assertion, fingerprints, origin, callback) {
|
||||
function onVerification(message) {
|
||||
if (message && this._checkVerifyResponse(message, fingerprints)) {
|
||||
callback(message);
|
||||
} else {
|
||||
this._warning("RTC identity: assertion validation failure", null, 0);
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
|
||||
let request = {
|
||||
type: "VERIFY",
|
||||
message: assertion,
|
||||
origin: origin
|
||||
};
|
||||
this._sendToIdp(request, "validation", onVerification.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Asks the IdP proxy for an identity assertion and, on success, enriches the
|
||||
* given SDP with an a=identity line and calls callback with the new SDP as
|
||||
* parameter. If no IdP is configured the original SDP (without a=identity
|
||||
* line) is passed to the callback.
|
||||
*/
|
||||
appendIdentityToSDP: function(sdp, fingerprint, origin, callback) {
|
||||
let onAssertion = function() {
|
||||
callback(this.wrapSdp(sdp), this.assertion);
|
||||
}.bind(this);
|
||||
|
||||
if (!this._idpchannel || this.assertion) {
|
||||
onAssertion();
|
||||
return;
|
||||
}
|
||||
|
||||
this._getIdentityAssertion(fingerprint, origin, onAssertion);
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts an identity assertion into the given SDP.
|
||||
*/
|
||||
wrapSdp: function(sdp) {
|
||||
addIdentityAttribute: function(sdp) {
|
||||
if (!this.assertion) {
|
||||
return sdp;
|
||||
}
|
||||
@ -263,103 +281,73 @@ PeerConnectionIdp.prototype = {
|
||||
// yes, we assume that this matches; if it doesn't something is *wrong*
|
||||
let match = sdp.match(PeerConnectionIdp._mLinePattern);
|
||||
return sdp.substring(0, match.index) +
|
||||
"a=identity:" + this.assertion + "\r\n" +
|
||||
'a=identity:' + this.assertion + '\r\n' +
|
||||
sdp.substring(match.index);
|
||||
},
|
||||
|
||||
getIdentityAssertion: function(fingerprint, callback) {
|
||||
if (!this._idpchannel) {
|
||||
this.reportError("assertion", "IdP not set");
|
||||
callback(null);
|
||||
return;
|
||||
/**
|
||||
* Asks the IdP proxy for an identity assertion. Don't call this unless you
|
||||
* have checked .enabled, or you really like exceptions.
|
||||
*/
|
||||
getIdentityAssertion: function(fingerprint) {
|
||||
if (!this.enabled) {
|
||||
this.reportError('assertion', 'no IdP set,' +
|
||||
' call setIdentityProvider() to set one');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let origin = Cu.getWebIDLCallerPrincipal().origin;
|
||||
this._getIdentityAssertion(fingerprint, origin, callback);
|
||||
},
|
||||
|
||||
_getIdentityAssertion: function(fingerprint, origin, callback) {
|
||||
let [algorithm, digest] = fingerprint.split(" ", 2);
|
||||
let message = {
|
||||
let [algorithm, digest] = fingerprint.split(' ', 2);
|
||||
let content = {
|
||||
fingerprint: [{
|
||||
algorithm: algorithm,
|
||||
digest: digest
|
||||
}]
|
||||
};
|
||||
let request = {
|
||||
type: "SIGN",
|
||||
message: JSON.stringify(message),
|
||||
username: this.username,
|
||||
origin: origin
|
||||
};
|
||||
let origin = Cu.getWebIDLCallerPrincipal().origin;
|
||||
|
||||
// catch the assertion, clean it up, warn if absent
|
||||
function trapAssertion(assertion) {
|
||||
if (!assertion) {
|
||||
this._warning("RTC identity: assertion generation failure", null, 0);
|
||||
this.assertion = null;
|
||||
} else {
|
||||
this.assertion = btoa(JSON.stringify(assertion));
|
||||
}
|
||||
callback(this.assertion);
|
||||
}
|
||||
let assertionPromise = this._idp.start()
|
||||
.then(idp => idp.generateAssertion(JSON.stringify(content),
|
||||
origin, this.username));
|
||||
|
||||
this._sendToIdp(request, "assertion", trapAssertion.bind(this));
|
||||
return this._safetyNet('assertion', assertionPromise)
|
||||
.then(assertion => {
|
||||
if (this._isValidAssertion(assertion)) {
|
||||
// save the base64+JSON assertion, since that is all that is used
|
||||
this.assertion = btoa(JSON.stringify(assertion));
|
||||
} else {
|
||||
if (assertion) {
|
||||
// only report an error for an invalid assertion
|
||||
// other paths generate more specific error reports
|
||||
this.reportError('assertion', 'invalid assertion generated');
|
||||
}
|
||||
this.assertion = null;
|
||||
}
|
||||
return this.assertion;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Packages a message and sends it to the IdP.
|
||||
* @param request (dictionary) the message to send
|
||||
* @param type (DOMString) the type of message (assertion/validation)
|
||||
* @param callback (function) the function to call with the results
|
||||
* Wraps a promise, adding a timeout guard on it so that it can't take longer
|
||||
* than the specified time. Returns a promise that always resolves; if there
|
||||
* is a problem the resolved value is undefined.
|
||||
*/
|
||||
_sendToIdp: function(request, type, callback) {
|
||||
this._idpchannel.send(request, this._wrapCallback(type, callback));
|
||||
},
|
||||
|
||||
_reportIdpError: function(type, message) {
|
||||
let args = {};
|
||||
let msg = "";
|
||||
if (message.type === "ERROR") {
|
||||
msg = message.error;
|
||||
} else {
|
||||
msg = JSON.stringify(message.message);
|
||||
if (message.type === "LOGINNEEDED") {
|
||||
args.loginUrl = message.loginUrl;
|
||||
}
|
||||
}
|
||||
this.reportError(type, "received response of type '" +
|
||||
message.type + "' from IdP: " + msg, args);
|
||||
},
|
||||
|
||||
/**
|
||||
* Wraps a callback, adding a timeout and ensuring that the callback doesn't
|
||||
* receive any message other than one where the IdP generated a "SUCCESS"
|
||||
* response.
|
||||
*/
|
||||
_wrapCallback: function(type, callback) {
|
||||
let timeout = this._win.setTimeout(function() {
|
||||
this.reportError(type, "IdP timeout for " + this._idpchannel + " " +
|
||||
(this._idpchannel.ready ? "[ready]" : "[not ready]"));
|
||||
timeout = null;
|
||||
callback(null);
|
||||
}.bind(this), this._timeout);
|
||||
|
||||
return function(message) {
|
||||
if (!timeout) {
|
||||
return;
|
||||
}
|
||||
this._win.clearTimeout(timeout);
|
||||
timeout = null;
|
||||
|
||||
let content = null;
|
||||
if (message.type === "SUCCESS") {
|
||||
content = message.message;
|
||||
} else {
|
||||
this._reportIdpError(type, message);
|
||||
}
|
||||
callback(content);
|
||||
}.bind(this);
|
||||
_safetyNet: function(type, p) {
|
||||
let done = false; // ... all because Promises don't expose state
|
||||
let timeoutPromise = delay(this._timeout)
|
||||
.then(() => {
|
||||
if (!done) {
|
||||
this.reportError(type, 'IdP timed out');
|
||||
}
|
||||
});
|
||||
let realPromise = p
|
||||
.catch(e => this.reportError(type, 'error reported by IdP: ' + e.message))
|
||||
.then(result => {
|
||||
done = true;
|
||||
return result;
|
||||
});
|
||||
// If timeoutPromise completes first, the returned value will be undefined,
|
||||
// just like when there is an error.
|
||||
return Promise.race([realPromise, timeoutPromise]);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -238,7 +238,7 @@ EXTRA_COMPONENTS += [
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.media += [
|
||||
'IdpProxy.jsm',
|
||||
'IdpSandbox.jsm',
|
||||
'PeerConnectionIdp.jsm',
|
||||
'RTCStatsReport.jsm',
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user