зеркало из https://github.com/mozilla/gecko-dev.git
381 строка
11 KiB
JavaScript
381 строка
11 KiB
JavaScript
/* 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/. */
|
|
|
|
var EXPORTED_SYMBOLS = ["PeerConnectionIdp"];
|
|
|
|
ChromeUtils.defineModuleGetter(
|
|
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, usernameHint, peerIdentity) {
|
|
this._resetAssertion();
|
|
this.provider = provider;
|
|
this.protocol = protocol;
|
|
this.username = usernameHint;
|
|
this.peeridentity = peerIdentity;
|
|
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;
|
|
this.username = null;
|
|
this.peeridentity = 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"
|
|
);
|
|
},
|
|
|
|
_getSessionLevelEnd(sdp) {
|
|
const match = sdp.match(PeerConnectionIdp._mLinePattern);
|
|
if (!match) {
|
|
return sdp.length;
|
|
}
|
|
return match.index;
|
|
},
|
|
|
|
_getIdentityFromSdp(sdp) {
|
|
// a=identity is session level
|
|
let idMatch;
|
|
const index = this._getSessionLevelEnd(sdp);
|
|
const sessionLevel = sdp.substring(0, 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 = Cc["@mozilla.org/network/idn-service;1"].getService(
|
|
Ci.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;
|
|
}
|
|
|
|
const index = this._getSessionLevelEnd(sdp);
|
|
return (
|
|
sdp.substring(0, index) +
|
|
"a=identity:" +
|
|
this.assertion +
|
|
"\r\n" +
|
|
sdp.substring(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 => {
|
|
let options = {
|
|
protocol: this.protocol,
|
|
usernameHint: this.username,
|
|
peerIdentity: this.peeridentity,
|
|
};
|
|
return this._wrapCrossCompartmentPromise(
|
|
idp.generateAssertion(JSON.stringify(content), origin, options)
|
|
);
|
|
})
|
|
.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]);
|
|
},
|
|
};
|