mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-08 20:47:44 +00:00
378 lines
12 KiB
JavaScript
378 lines
12 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, "IdpProxy",
|
|
"resource://gre/modules/media/IdpProxy.jsm");
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function PeerConnectionIdp(window, timeout, warningFunc, dispatchEventFunc) {
|
|
this._win = window;
|
|
this._timeout = timeout || 5000;
|
|
this._warning = warningFunc;
|
|
this._dispatchEvent = dispatchEventFunc;
|
|
|
|
this.assertion = null;
|
|
this.provider = null;
|
|
}
|
|
|
|
(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 = {
|
|
setIdentityProvider: function(provider, protocol, username) {
|
|
this.provider = provider;
|
|
this.protocol = protocol;
|
|
this.username = username;
|
|
if (this._idpchannel) {
|
|
if (this._idpchannel.isSame(provider, protocol)) {
|
|
return;
|
|
}
|
|
this._idpchannel.close();
|
|
}
|
|
this._idpchannel = new IdpProxy(provider, protocol);
|
|
},
|
|
|
|
close: function() {
|
|
this.assertion = null;
|
|
this.provider = null;
|
|
if (this._idpchannel) {
|
|
this._idpchannel.close();
|
|
this._idpchannel = null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Generate an error event of the identified type;
|
|
* and put a little more precise information in the console.
|
|
*/
|
|
reportError: function(type, message, extra) {
|
|
let args = {
|
|
idp: this.provider,
|
|
protocol: this.protocol
|
|
};
|
|
if (extra) {
|
|
Object.keys(extra).forEach(function(k) {
|
|
args[k] = extra[k];
|
|
});
|
|
}
|
|
this._warning("RTC identity: " + message, null, 0);
|
|
let ev = new this._win.RTCPeerConnectionIdentityErrorEvent('idp' + type + 'error', args);
|
|
this._dispatchEvent(ev);
|
|
},
|
|
|
|
_getFingerprintFromSdp: function(sdp) {
|
|
let sections = sdp.split(PeerConnectionIdp._mLinePattern);
|
|
let attributes = sections.map(function(sect) {
|
|
let m = sect.match(PeerConnectionIdp._fingerprintPattern);
|
|
if (m) {
|
|
let remainder = sect.substring(m.index + m[0].length);
|
|
if (!remainder.match(PeerConnectionIdp._fingerprintPattern)) {
|
|
return { algorithm: m[1], digest: m[2] };
|
|
}
|
|
this.reportError("validation", "two fingerprint values" +
|
|
" in same media section are not supported");
|
|
// we have to return non-falsy here so that a media section doesn't
|
|
// accidentally fall back to the session-level stuff (which is bad)
|
|
return "error";
|
|
}
|
|
// return undefined unless there is exactly one match
|
|
}, this);
|
|
|
|
let sessionLevel = attributes.shift();
|
|
attributes = attributes.map(function(sectionLevel) {
|
|
return sectionLevel || sessionLevel;
|
|
});
|
|
|
|
let first = attributes.shift();
|
|
function sameAsFirst(attr) {
|
|
return typeof attr === "object" &&
|
|
first.algorithm === attr.algorithm &&
|
|
first.digest === attr.digest;
|
|
}
|
|
|
|
if (typeof first === "object" && attributes.every(sameAsFirst)) {
|
|
return first;
|
|
}
|
|
// undefined!
|
|
},
|
|
|
|
_getIdentityFromSdp: function(sdp) {
|
|
// a=identity is session level
|
|
let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern);
|
|
let sessionLevel = sdp.substring(0, mLineMatch.index);
|
|
let idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
|
|
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");
|
|
}
|
|
// undefined!
|
|
},
|
|
|
|
/**
|
|
* Queues a task to verify 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.
|
|
*/
|
|
verifyIdentityFromSDP: function(sdp, callback) {
|
|
let identity = this._getIdentityFromSdp(sdp);
|
|
let fingerprint = this._getFingerprintFromSdp(sdp);
|
|
// it's safe to use the fingerprint we got from the SDP here,
|
|
// only because we ensure that there is only one
|
|
if (!fingerprint || !identity) {
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
|
|
this._verifyIdentity(identity.assertion, fingerprint, callback);
|
|
},
|
|
|
|
/**
|
|
* Checks that the name in the identity provided by the IdP is OK.
|
|
*
|
|
* @param name (string) the name to validate
|
|
* @returns (string) an error message, iff the name isn't good
|
|
*/
|
|
_validateName: function(name) {
|
|
if (typeof name !== "string") {
|
|
return "name not a string";
|
|
}
|
|
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;
|
|
}
|
|
return "missing authority in name from IdP";
|
|
},
|
|
|
|
// 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, fingerprint) {
|
|
let warn = function(msg) {
|
|
this.reportError("validation",
|
|
"assertion validation failure: " + msg);
|
|
}.bind(this);
|
|
|
|
try {
|
|
let contents = JSON.parse(message.contents);
|
|
if (typeof contents.fingerprint !== "object") {
|
|
warn("fingerprint is not an object");
|
|
} else if (contents.fingerprint.digest !== fingerprint.digest ||
|
|
contents.fingerprint.algorithm !== fingerprint.algorithm) {
|
|
warn("fingerprint does not match");
|
|
} else {
|
|
let error = this._validateName(message.identity);
|
|
if (error) {
|
|
warn(error);
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
} catch(e) {
|
|
warn("invalid JSON in content");
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Asks the IdP proxy to verify an identity.
|
|
*/
|
|
_verifyIdentity: function(
|
|
assertion, fingerprint, callback) {
|
|
function onVerification(message) {
|
|
if (message && this._checkVerifyResponse(message, fingerprint)) {
|
|
callback(message);
|
|
} else {
|
|
this._warning("RTC identity: assertion validation failure", null, 0);
|
|
callback(null);
|
|
}
|
|
}
|
|
|
|
let request = {
|
|
type: "VERIFY",
|
|
message: assertion
|
|
};
|
|
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, callback) {
|
|
let onAssertion = function() {
|
|
callback(this.wrapSdp(sdp), this.assertion);
|
|
}.bind(this);
|
|
|
|
if (!this._idpchannel || this.assertion) {
|
|
onAssertion();
|
|
return;
|
|
}
|
|
|
|
this._getIdentityAssertion(fingerprint, onAssertion);
|
|
},
|
|
|
|
/**
|
|
* Inserts an identity assertion into the given SDP.
|
|
*/
|
|
wrapSdp: function(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);
|
|
},
|
|
|
|
getIdentityAssertion: function(fingerprint, callback) {
|
|
if (!this._idpchannel) {
|
|
this.reportError("assertion", "IdP not set");
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
this._getIdentityAssertion(fingerprint, callback);
|
|
},
|
|
|
|
_getIdentityAssertion: function(fingerprint, callback) {
|
|
let [algorithm, digest] = fingerprint.split(" ");
|
|
let message = {
|
|
fingerprint: {
|
|
algorithm: algorithm,
|
|
digest: digest
|
|
}
|
|
};
|
|
let request = {
|
|
type: "SIGN",
|
|
message: JSON.stringify(message),
|
|
username: this.username
|
|
};
|
|
|
|
// 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);
|
|
}
|
|
|
|
this._sendToIdp(request, "assertion", trapAssertion.bind(this));
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
_sendToIdp: function(request, type, callback) {
|
|
request.origin = Cu.getWebIDLCallerPrincipal().origin;
|
|
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);
|
|
}
|
|
};
|
|
|
|
this.PeerConnectionIdp = PeerConnectionIdp;
|