gecko-dev/dom/media/PeerConnectionIdp.jsm

342 строки
11 KiB
JavaScript

/* jshint moz:true, browser:true */
/* 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/. */
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, "IdpSandbox",
"resource://gre/modules/media/IdpSandbox.jsm");
/**
* Creates an IdP helper.
*
* @param win (object) the window we are working for
* @param timeout (int) the timeout in milliseconds
*/
function PeerConnectionIdp(win, timeout) {
this._win = win;
this._timeout = timeout || 5000;
this.provider = null;
this._resetAssertion();
}
(function() {
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");
})();
PeerConnectionIdp.prototype = {
get enabled() {
return !!this._idp;
},
_resetAssertion() {
this.assertion = null;
this.idpLoginUrl = null;
},
setIdentityProvider(provider, protocol, username) {
this._resetAssertion();
this.provider = provider;
this.protocol = protocol || "default";
this.username = username;
if (this._idp) {
if (this._idp.isSame(provider, protocol)) {
return; // noop
}
this._idp.stop();
}
this._idp = new IdpSandbox(provider, protocol, this._win);
},
// start the IdP and do some error fixup
start() {
return this._idp.start()
.catch(e => {
throw new this._win.DOMException(e.message, "IdpError");
});
},
close() {
this._resetAssertion();
this.provider = null;
this.protocol = null;
if (this._idp) {
this._idp.stop();
this._idp = null;
}
},
_getFingerprintsFromSdp(sdp) {
let fingerprints = {};
let m = sdp.match(PeerConnectionIdp._fingerprintPattern);
while (m) {
fingerprints[m[0]] = { algorithm: m[1], digest: m[2] };
sdp = sdp.substring(m.index + m[0].length);
m = sdp.match(PeerConnectionIdp._fingerprintPattern);
}
return Object.keys(fingerprints).map(k => fingerprints[k]);
},
_isValidAssertion(assertion) {
return assertion && assertion.idp &&
typeof assertion.idp.domain === "string" &&
(!assertion.idp.protocol ||
typeof assertion.idp.protocol === "string") &&
typeof assertion.assertion === "string";
},
_getIdentityFromSdp(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);
}
if (!idMatch) {
return undefined; // undefined === no identity
}
let assertion;
try {
assertion = JSON.parse(atob(idMatch[1]));
} catch (e) {
throw new this._win.DOMException("invalid identity assertion: " + e,
"InvalidSessionDescriptionError");
}
if (!this._isValidAssertion(assertion)) {
throw new this._win.DOMException("assertion missing idp/idp.domain/assertion",
"InvalidSessionDescriptionError");
}
return assertion;
},
/**
* 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. We still rely on
* the fact that the RTCPeerConnection won't connect to a peer if the
* fingerprint of the certificate they offer doesn't appear in the SDP.
*/
verifyIdentityFromSDP(sdp, origin) {
let identity = this._getIdentityFromSdp(sdp);
let fingerprints = this._getFingerprintsFromSdp(sdp);
if (!identity || fingerprints.length <= 0) {
return this._win.Promise.resolve(); // undefined result = no identity
}
this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
return this._verifyIdentity(identity.assertion, fingerprints, origin);
},
/**
* Checks that the name in the identity provided by the IdP is OK.
*
* @param name (string) the name to validate
* @throws if the name isn't valid
*/
_validateName(name) {
let error = msg => {
throw new this._win.DOMException("assertion name error: " + msg,
"IdpError");
};
if (typeof name !== "string") {
error("name not a string");
}
let atIdx = name.indexOf("@");
if (atIdx <= 0) {
error("missing authority in name from IdP");
}
// 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)) {
error('name "' + name +
'" doesn\'t match IdP: "' + this.provider + '"');
}
},
/**
* 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.
*/
_checkValidation(validation, sdpFingerprints) {
let error = msg => {
throw new this._win.DOMException("IdP validation error: " + msg,
"IdpError");
};
if (!this.provider) {
error("IdP closed");
}
if (typeof validation !== "object" ||
typeof validation.contents !== "string" ||
typeof validation.identity !== "string") {
error("no payload in validation response");
}
let fingerprints;
try {
fingerprints = JSON.parse(validation.contents).fingerprint;
} catch (e) {
error("invalid JSON");
}
let isFingerprint = f =>
(typeof f.digest === "string") &&
(typeof f.algorithm === "string");
if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
error("fingerprints must be an array of objects" +
" with digest and algorithm attributes");
}
// everything in `innerSet` is found in `outerSet`
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)) {
error("the fingerprints must be covered by the assertion");
}
this._validateName(validation.identity);
return validation;
},
/**
* Asks the IdP proxy to verify an identity assertion.
*/
_verifyIdentity(assertion, fingerprints, origin) {
let p = this.start()
.then(idp => this._wrapCrossCompartmentPromise(
idp.validateAssertion(assertion, origin)))
.then(validation => this._checkValidation(validation, fingerprints));
return this._applyTimeout(p);
},
/**
* Enriches the given SDP with an `a=identity` line. getIdentityAssertion()
* must have already run successfully, otherwise this does nothing to the sdp.
*/
addIdentityAttribute(sdp) {
if (!this.assertion) {
return sdp;
}
// 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" +
sdp.substring(match.index);
},
/**
* Asks the IdP proxy for an identity assertion. Don't call this unless you
* have checked .enabled, or you really like exceptions. Also, don't call
* this when another call is still running, because it's not certain which
* call will finish first and the final state will be similarly uncertain.
*/
getIdentityAssertion(fingerprint, origin) {
if (!this.enabled) {
throw new this._win.DOMException(
"no IdP set, call setIdentityProvider() to set one", "InvalidStateError");
}
let [algorithm, digest] = fingerprint.split(" ", 2);
let content = {
fingerprint: [{
algorithm,
digest
}]
};
this._resetAssertion();
let p = this.start()
.then(idp => this._wrapCrossCompartmentPromise(
idp.generateAssertion(JSON.stringify(content),
origin, this.username)))
.then(assertion => {
if (!this._isValidAssertion(assertion)) {
throw new this._win.DOMException("IdP generated invalid assertion",
"IdpError");
}
// save the base64+JSON assertion, since that is all that is used
this.assertion = btoa(JSON.stringify(assertion));
return this.assertion;
});
return this._applyTimeout(p);
},
/**
* Promises generated by the sandbox need to be very carefully treated so that
* they can chain into promises in the `this._win` compartment. Results need
* to be cloned across; errors need to be converted.
*/
_wrapCrossCompartmentPromise(sandboxPromise) {
return new this._win.Promise((resolve, reject) => {
sandboxPromise.then(
result => resolve(Cu.cloneInto(result, this._win)),
e => {
let message = "" + (e.message || JSON.stringify(e) || "IdP error");
if (e.name === "IdpLoginError") {
if (typeof e.loginUrl === "string") {
this.idpLoginUrl = e.loginUrl;
}
reject(new this._win.DOMException(message, "IdpLoginError"));
} else {
reject(new this._win.DOMException(message, "IdpError"));
}
});
});
},
/**
* Wraps a promise, adding a timeout guard on it so that it can't take longer
* than the specified time. Returns a promise that rejects if the timeout
* elapses before `p` resolves.
*/
_applyTimeout(p) {
let timeout = new this._win.Promise(
r => this._win.setTimeout(r, this._timeout))
.then(() => {
throw new this._win.DOMException("IdP timed out", "IdpError");
});
return this._win.Promise.race([ timeout, p ]);
}
};
this.PeerConnectionIdp = PeerConnectionIdp;