2010-01-21 10:41:24 -08:00
|
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
|
|
*
|
|
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
|
|
* the License. You may obtain a copy of the License at
|
|
|
|
* http://www.mozilla.org/MPL/
|
|
|
|
*
|
|
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
|
|
* for the specific language governing rights and limitations under the
|
|
|
|
* License.
|
|
|
|
*
|
|
|
|
* The Original Code is the Content Security Policy data structures.
|
|
|
|
*
|
|
|
|
* The Initial Developer of the Original Code is
|
|
|
|
* Mozilla Corporation
|
|
|
|
*
|
|
|
|
* Contributor(s):
|
|
|
|
* Sid Stamm <sid@mozilla.com>
|
|
|
|
*
|
|
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
|
|
* the provisions above, a recipient may use your version of this file under
|
|
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
|
|
*
|
|
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Content Security Policy Utilities
|
|
|
|
*
|
|
|
|
* Overview
|
|
|
|
* This contains a set of classes and utilities for CSP. It is in this
|
|
|
|
* separate file for testing purposes.
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Module stuff
|
|
|
|
var EXPORTED_SYMBOLS = ["CSPRep", "CSPSourceList", "CSPSource",
|
|
|
|
"CSPHost", "CSPWarning", "CSPError", "CSPdebug"];
|
|
|
|
|
|
|
|
|
|
|
|
// these are not exported
|
|
|
|
var gIoService = Components.classes["@mozilla.org/network/io-service;1"]
|
|
|
|
.getService(Components.interfaces.nsIIOService);
|
|
|
|
|
|
|
|
var gETLDService = Components.classes["@mozilla.org/network/effective-tld-service;1"]
|
|
|
|
.getService(Components.interfaces.nsIEffectiveTLDService);
|
|
|
|
|
|
|
|
|
|
|
|
function CSPWarning(aMsg) {
|
|
|
|
// customize this to redirect output.
|
|
|
|
aMsg = 'CSP WARN: ' + aMsg + "\n";
|
|
|
|
dump(aMsg);
|
|
|
|
Components.classes["@mozilla.org/consoleservice;1"]
|
|
|
|
.getService(Components.interfaces.nsIConsoleService)
|
|
|
|
.logStringMessage(aMsg);
|
|
|
|
}
|
|
|
|
function CSPError(aMsg) {
|
|
|
|
aMsg = 'CSP ERROR: ' + aMsg + "\n";
|
|
|
|
dump(aMsg);
|
|
|
|
Components.classes["@mozilla.org/consoleservice;1"]
|
|
|
|
.getService(Components.interfaces.nsIConsoleService)
|
|
|
|
.logStringMessage(aMsg);
|
|
|
|
}
|
|
|
|
function CSPdebug(aMsg) {
|
|
|
|
aMsg = 'CSP debug: ' + aMsg + "\n";
|
|
|
|
dump(aMsg);
|
|
|
|
Components.classes["@mozilla.org/consoleservice;1"]
|
|
|
|
.getService(Components.interfaces.nsIConsoleService)
|
|
|
|
.logStringMessage(aMsg);
|
|
|
|
}
|
|
|
|
|
|
|
|
//:::::::::::::::::::::::: CLASSES :::::::::::::::::::::::::://
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Class that represents a parsed policy structure.
|
|
|
|
*/
|
|
|
|
function CSPRep() {
|
|
|
|
// this gets set to true when the policy is done parsing, or when a
|
|
|
|
// URI-borne policy has finished loading.
|
|
|
|
this._isInitialized = false;
|
|
|
|
|
|
|
|
this._allowEval = false;
|
|
|
|
this._allowInlineScripts = false;
|
|
|
|
|
|
|
|
// don't auto-populate _directives, so it is easier to find bugs
|
|
|
|
this._directives = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
CSPRep.SRC_DIRECTIVES = {
|
|
|
|
ALLOW: "allow",
|
|
|
|
SCRIPT_SRC: "script-src",
|
|
|
|
STYLE_SRC: "style-src",
|
|
|
|
MEDIA_SRC: "media-src",
|
|
|
|
IMG_SRC: "img-src",
|
|
|
|
OBJECT_SRC: "object-src",
|
|
|
|
FRAME_SRC: "frame-src",
|
|
|
|
FRAME_ANCESTORS: "frame-ancestors",
|
|
|
|
FONT_SRC: "font-src",
|
|
|
|
XHR_SRC: "xhr-src"
|
|
|
|
};
|
|
|
|
|
|
|
|
CSPRep.URI_DIRECTIVES = {
|
|
|
|
REPORT_URI: "report-uri", /* list of URIs */
|
|
|
|
POLICY_URI: "policy-uri" /* single URI */
|
|
|
|
};
|
|
|
|
|
|
|
|
CSPRep.OPTIONS_DIRECTIVE = "options";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Factory to create a new CSPRep, parsed from a string.
|
|
|
|
*
|
|
|
|
* @param aStr
|
|
|
|
* string rep of a CSP
|
|
|
|
* @param self (optional)
|
|
|
|
* string or CSPSource representing the "self" source
|
|
|
|
* @returns
|
|
|
|
* an instance of CSPRep
|
|
|
|
*/
|
|
|
|
CSPRep.fromString = function(aStr, self) {
|
|
|
|
var SD = CSPRep.SRC_DIRECTIVES;
|
|
|
|
var UD = CSPRep.URI_DIRECTIVES;
|
|
|
|
var aCSPR = new CSPRep();
|
|
|
|
aCSPR._originalText = aStr;
|
|
|
|
|
|
|
|
var dirs = aStr.split(";");
|
|
|
|
|
|
|
|
directive:
|
|
|
|
for each(var dir in dirs) {
|
|
|
|
dir = dir.trim();
|
|
|
|
var dirname = dir.split(/\s+/)[0];
|
|
|
|
var dirvalue = dir.substring(dirname.length).trim();
|
|
|
|
|
|
|
|
// OPTIONS DIRECTIVE ////////////////////////////////////////////////
|
|
|
|
if (dirname === CSPRep.OPTIONS_DIRECTIVE) {
|
|
|
|
// grab value tokens and interpret them
|
|
|
|
var options = dirvalue.split(/\s+/);
|
|
|
|
for each (var opt in options) {
|
|
|
|
if (opt === "inline-script")
|
|
|
|
aCSPR._allowInlineScripts = true;
|
|
|
|
else if (opt === "eval-script")
|
|
|
|
aCSPR._allowEval = true;
|
|
|
|
else
|
|
|
|
CSPWarning("don't understand option '" + opt + "'. Ignoring it.");
|
|
|
|
}
|
|
|
|
continue directive;
|
|
|
|
}
|
|
|
|
|
|
|
|
// SOURCE DIRECTIVES ////////////////////////////////////////////////
|
|
|
|
for each(var sdi in SD) {
|
|
|
|
if (dirname === sdi) {
|
|
|
|
// process dirs, and enforce that 'self' is defined.
|
|
|
|
var dv = CSPSourceList.fromString(dirvalue, self, true);
|
|
|
|
if (dv) {
|
|
|
|
aCSPR._directives[sdi] = dv;
|
|
|
|
continue directive;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// REPORT URI ///////////////////////////////////////////////////////
|
|
|
|
if (dirname === UD.REPORT_URI) {
|
|
|
|
// might be space-separated list of URIs
|
|
|
|
var uriStrings = dirvalue.split(/\s+/);
|
|
|
|
var okUriStrings = [];
|
|
|
|
var selfUri = self ? gIoService.newURI(self.toString(),null,null) : null;
|
|
|
|
|
|
|
|
// Verify that each report URI is in the same etld + 1
|
|
|
|
// if "self" is defined, and just that it's valid otherwise.
|
|
|
|
for (let i in uriStrings) {
|
|
|
|
try {
|
|
|
|
var uri = gIoService.newURI(uriStrings[i],null,null);
|
|
|
|
if (self) {
|
2010-06-09 09:48:39 -07:00
|
|
|
if (gETLDService.getBaseDomain(uri) ===
|
2010-01-21 10:41:24 -08:00
|
|
|
gETLDService.getBaseDomain(selfUri)) {
|
|
|
|
okUriStrings.push(uriStrings[i]);
|
|
|
|
} else {
|
|
|
|
CSPWarning("can't use report URI from non-matching eTLD+1: "
|
|
|
|
+ gETLDService.getBaseDomain(uri));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch(e) {
|
2010-06-09 09:48:39 -07:00
|
|
|
switch (e.result) {
|
|
|
|
case Components.results.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS:
|
|
|
|
case Components.results.NS_ERROR_HOST_IS_IP_ADDRESS:
|
|
|
|
if (uri.host === selfUri.host) {
|
|
|
|
okUriStrings.push(uriStrings[i]);
|
|
|
|
} else {
|
|
|
|
CSPWarning("page on " + selfUri.host + " cannot send reports to " + uri.host);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
CSPWarning("couldn't parse report URI: " + uriStrings[i]);
|
|
|
|
break;
|
|
|
|
}
|
2010-01-21 10:41:24 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
aCSPR._directives[UD.REPORT_URI] = okUriStrings.join(' ');
|
|
|
|
continue directive;
|
|
|
|
}
|
|
|
|
|
|
|
|
// POLICY URI //////////////////////////////////////////////////////////
|
|
|
|
if (dirname === UD.POLICY_URI) {
|
|
|
|
// POLICY_URI can only be alone
|
|
|
|
if (aCSPR._directives.length > 0 || dirs.length > 1) {
|
|
|
|
CSPError("policy-uri directive can only appear alone");
|
|
|
|
return CSPRep.fromString("allow 'none'");
|
|
|
|
}
|
|
|
|
|
|
|
|
var uri = '';
|
|
|
|
try {
|
|
|
|
uri = gIoService.newURI(dirvalue, null, null);
|
|
|
|
} catch(e) {
|
|
|
|
CSPError("could not parse URI in policy URI: " + dirvalue);
|
|
|
|
return CSPRep.fromString("allow 'none'");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify that policy URI comes from the same origin
|
|
|
|
if (self) {
|
|
|
|
var selfUri = gIoService.newURI(self.toString(), null, null);
|
|
|
|
if (selfUri.host !== uri.host){
|
|
|
|
CSPError("can't fetch policy uri from non-matching hostname: " + uri.host);
|
|
|
|
return CSPRep.fromString("allow 'none'");
|
|
|
|
}
|
|
|
|
if (selfUri.port !== uri.port){
|
|
|
|
CSPError("can't fetch policy uri from non-matching port: " + uri.port);
|
|
|
|
return CSPRep.fromString("allow 'none'");
|
|
|
|
}
|
|
|
|
if (selfUri.scheme !== uri.scheme){
|
|
|
|
CSPError("can't fetch policy uri from non-matching scheme: " + uri.scheme);
|
|
|
|
return CSPRep.fromString("allow 'none'");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
|
|
.createInstance(Components.interfaces.nsIXMLHttpRequest);
|
|
|
|
|
|
|
|
// insert error hook
|
|
|
|
req.onerror = CSPError;
|
|
|
|
|
|
|
|
// synchronous -- otherwise we need to architect a callback into the
|
|
|
|
// xpcom component so that whomever creates the policy object gets
|
|
|
|
// notified when it's loaded and ready to go.
|
|
|
|
req.open("GET", dirvalue, false);
|
|
|
|
|
|
|
|
// make request anonymous
|
|
|
|
// This prevents sending cookies with the request, in case the policy URI
|
|
|
|
// is injected, it can't be abused for CSRF.
|
|
|
|
req.channel.loadFlags |= Components.interfaces.nsIChannel.LOAD_ANONYMOUS;
|
|
|
|
|
|
|
|
req.send(null);
|
|
|
|
if (req.status == 200) {
|
|
|
|
aCSPR = CSPRep.fromString(req.responseText, self);
|
|
|
|
// remember where we got the policy
|
|
|
|
aCSPR._directives[UD.POLICY_URI] = dirvalue;
|
|
|
|
return aCSPR;
|
|
|
|
}
|
|
|
|
CSPError("Error fetching policy URI: server response was " + req.status);
|
|
|
|
return CSPRep.fromString("allow 'none'");
|
|
|
|
}
|
|
|
|
|
|
|
|
// UNIDENTIFIED DIRECTIVE /////////////////////////////////////////////
|
|
|
|
CSPWarning("Couldn't process unknown directive '" + dirname + "'");
|
|
|
|
|
|
|
|
} // end directive: loop
|
|
|
|
|
2010-08-16 10:12:28 -07:00
|
|
|
// if makeExplicit fails for any reason, default to allow 'none'. This
|
|
|
|
// includes the case where "allow" is not present.
|
|
|
|
if (aCSPR.makeExplicit())
|
|
|
|
return aCSPR;
|
|
|
|
return CSPRep.fromString("allow 'none'", self);
|
2010-01-21 10:41:24 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
CSPRep.prototype = {
|
|
|
|
/**
|
|
|
|
* Returns a space-separated list of all report uris defined, or 'none' if there are none.
|
|
|
|
*/
|
|
|
|
getReportURIs:
|
|
|
|
function() {
|
|
|
|
if (!this._directives[CSPRep.URI_DIRECTIVES.REPORT_URI])
|
|
|
|
return "";
|
|
|
|
return this._directives[CSPRep.URI_DIRECTIVES.REPORT_URI];
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Compares this CSPRep instance to another.
|
|
|
|
*/
|
|
|
|
equals:
|
|
|
|
function(that) {
|
|
|
|
if (this._directives.length != that._directives.length) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
for (var i in this._directives) {
|
|
|
|
if (!that._directives[i] || !this._directives[i].equals(that._directives[i])) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return (this.allowsInlineScripts === that.allowsInlineScripts)
|
|
|
|
&& (this.allowsEvalInScripts === that.allowsEvalInScripts);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates string representation of the policy. Should be fairly similar
|
|
|
|
* to the original.
|
|
|
|
*/
|
|
|
|
toString:
|
|
|
|
function csp_toString() {
|
|
|
|
var dirs = [];
|
|
|
|
|
|
|
|
if (this._allowEval || this._allowInlineScripts) {
|
|
|
|
dirs.push("options " + (this._allowEval ? "eval-script" : "")
|
|
|
|
+ (this._allowInlineScripts ? "inline-script" : ""));
|
|
|
|
}
|
|
|
|
for (var i in this._directives) {
|
|
|
|
if (this._directives[i]) {
|
|
|
|
dirs.push(i + " " + this._directives[i].toString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return dirs.join("; ");
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if this policy accepts a URI.
|
|
|
|
* @param aContext
|
|
|
|
* one of the SRC_DIRECTIVES defined above
|
|
|
|
* @returns
|
|
|
|
* true if the policy permits the URI in given context.
|
|
|
|
*/
|
|
|
|
permits:
|
|
|
|
function csp_permits(aURI, aContext) {
|
|
|
|
if (!aURI) return false;
|
|
|
|
|
|
|
|
// GLOBALLY ALLOW "about:" SCHEME
|
|
|
|
if (aURI instanceof String && aURI.substring(0,6) === "about:")
|
|
|
|
return true;
|
|
|
|
if (aURI instanceof Components.interfaces.nsIURI && aURI.scheme === "about")
|
|
|
|
return true;
|
|
|
|
|
|
|
|
// make sure the context is valid
|
|
|
|
for (var i in CSPRep.SRC_DIRECTIVES) {
|
|
|
|
if (CSPRep.SRC_DIRECTIVES[i] === aContext) {
|
|
|
|
return this._directives[aContext].permits(aURI);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Intersects with another CSPRep, deciding the subset policy
|
|
|
|
* that should be enforced, and returning a new instance.
|
|
|
|
* @param aCSPRep
|
|
|
|
* a CSPRep instance to use as "other" CSP
|
|
|
|
* @returns
|
|
|
|
* a new CSPRep instance of the intersection
|
|
|
|
*/
|
|
|
|
intersectWith:
|
|
|
|
function cspsd_intersectWith(aCSPRep) {
|
|
|
|
var newRep = new CSPRep();
|
|
|
|
|
|
|
|
for (var dir in CSPRep.SRC_DIRECTIVES) {
|
|
|
|
var dirv = CSPRep.SRC_DIRECTIVES[dir];
|
|
|
|
newRep._directives[dirv] = this._directives[dirv]
|
|
|
|
.intersectWith(aCSPRep._directives[dirv]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// REPORT_URI
|
|
|
|
var reportURIDir = CSPRep.URI_DIRECTIVES.REPORT_URI;
|
|
|
|
if (this._directives[reportURIDir] && aCSPRep._directives[reportURIDir]) {
|
|
|
|
newRep._directives[reportURIDir] =
|
|
|
|
this._directives[reportURIDir].concat(aCSPRep._directives[reportURIDir]);
|
|
|
|
}
|
|
|
|
else if (this._directives[reportURIDir]) {
|
|
|
|
// blank concat makes a copy of the string.
|
|
|
|
newRep._directives[reportURIDir] = this._directives[reportURIDir].concat();
|
|
|
|
}
|
|
|
|
else if (aCSPRep._directives[reportURIDir]) {
|
|
|
|
// blank concat makes a copy of the string.
|
|
|
|
newRep._directives[reportURIDir] = aCSPRep._directives[reportURIDir].concat();
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var dir in CSPRep.SRC_DIRECTIVES) {
|
|
|
|
var dirv = CSPRep.SRC_DIRECTIVES[dir];
|
|
|
|
newRep._directives[dirv] = this._directives[dirv]
|
|
|
|
.intersectWith(aCSPRep._directives[dirv]);
|
|
|
|
}
|
|
|
|
|
|
|
|
newRep._allowEval = this.allowsEvalInScripts
|
|
|
|
&& aCSPRep.allowsEvalInScripts;
|
|
|
|
|
|
|
|
newRep._allowInlineScripts = this.allowsInlineScripts
|
|
|
|
&& aCSPRep.allowsInlineScripts;
|
|
|
|
|
|
|
|
return newRep;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Copies default source list to each unspecified directive.
|
|
|
|
* @returns
|
|
|
|
* true if the makeExplicit succeeds
|
|
|
|
* false if it fails (for some weird reason)
|
|
|
|
*/
|
|
|
|
makeExplicit:
|
|
|
|
function cspsd_makeExplicit() {
|
|
|
|
var SD = CSPRep.SRC_DIRECTIVES;
|
|
|
|
var allowDir = this._directives[SD.ALLOW];
|
|
|
|
if (!allowDir) {
|
2010-08-16 10:12:28 -07:00
|
|
|
CSPWarning("'allow' directive required but not present. Reverting to \"allow 'none'\"");
|
2010-01-21 10:41:24 -08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var dir in SD) {
|
|
|
|
var dirv = SD[dir];
|
|
|
|
if (dirv === SD.ALLOW) continue;
|
|
|
|
if (!this._directives[dirv]) {
|
2010-06-09 09:48:42 -07:00
|
|
|
// implicit directive, make explicit.
|
|
|
|
// All but frame-ancestors directive inherit from 'allow' (bug 555068)
|
|
|
|
if (dirv === SD.FRAME_ANCESTORS)
|
|
|
|
this._directives[dirv] = CSPSourceList.fromString("*");
|
|
|
|
else
|
|
|
|
this._directives[dirv] = allowDir.clone();
|
2010-01-21 10:41:24 -08:00
|
|
|
this._directives[dirv]._isImplicit = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this._isInitialized = true;
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if "eval" is enabled through the "eval" keyword.
|
|
|
|
*/
|
|
|
|
get allowsEvalInScripts () {
|
|
|
|
return this._allowEval;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if inline scripts are enabled through the "inline"
|
|
|
|
* keyword.
|
|
|
|
*/
|
|
|
|
get allowsInlineScripts () {
|
|
|
|
return this._allowInlineScripts;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
|
|
* Class to represent a list of sources
|
|
|
|
*/
|
|
|
|
function CSPSourceList() {
|
|
|
|
this._sources = [];
|
|
|
|
this._permitAllSources = false;
|
|
|
|
|
|
|
|
// Set to true when this list is created using "makeExplicit()"
|
|
|
|
// It's useful to know this when reporting the directive that was violated.
|
|
|
|
this._isImplicit = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Factory to create a new CSPSourceList, parsed from a string.
|
|
|
|
*
|
|
|
|
* @param aStr
|
|
|
|
* string rep of a CSP Source List
|
|
|
|
* @param self (optional)
|
|
|
|
* string or CSPSource representing the "self" source
|
|
|
|
* @param enforceSelfChecks (optional)
|
|
|
|
* if present, and "true", will check to be sure "self" has the
|
|
|
|
* appropriate values to inherit when they are omitted from the source.
|
|
|
|
* @returns
|
|
|
|
* an instance of CSPSourceList
|
|
|
|
*/
|
|
|
|
CSPSourceList.fromString = function(aStr, self, enforceSelfChecks) {
|
|
|
|
// Source list is:
|
|
|
|
// <host-dir-value> ::= <source-list>
|
|
|
|
// | "'none'"
|
|
|
|
// <source-list> ::= <source>
|
|
|
|
// | <source-list>" "<source>
|
|
|
|
|
|
|
|
var slObj = new CSPSourceList();
|
|
|
|
if (aStr === "'none'")
|
|
|
|
return slObj;
|
|
|
|
|
|
|
|
if (aStr === "*") {
|
|
|
|
slObj._permitAllSources = true;
|
|
|
|
return slObj;
|
|
|
|
}
|
|
|
|
|
|
|
|
var tokens = aStr.split(/\s+/);
|
|
|
|
for (var i in tokens) {
|
|
|
|
if (tokens[i] === "") continue;
|
|
|
|
var src = CSPSource.create(tokens[i], self, enforceSelfChecks);
|
|
|
|
if (!src) {
|
|
|
|
CSPWarning("Failed to parse unrecoginzied source " + tokens[i]);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
slObj._sources.push(src);
|
|
|
|
}
|
|
|
|
|
|
|
|
return slObj;
|
|
|
|
};
|
|
|
|
|
|
|
|
CSPSourceList.prototype = {
|
|
|
|
/**
|
|
|
|
* Compares one CSPSourceList to another.
|
|
|
|
*
|
|
|
|
* @param that
|
|
|
|
* another CSPSourceList
|
|
|
|
* @returns
|
|
|
|
* true if they have the same data
|
|
|
|
*/
|
|
|
|
equals:
|
|
|
|
function(that) {
|
|
|
|
if (that._sources.length != this._sources.length) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// sort both arrays and compare like a zipper
|
|
|
|
// XXX (sid): I think we can make this more efficient
|
|
|
|
var sortfn = function(a,b) {
|
|
|
|
return a.toString() > b.toString();
|
|
|
|
};
|
|
|
|
var a_sorted = this._sources.sort(sortfn);
|
|
|
|
var b_sorted = that._sources.sort(sortfn);
|
|
|
|
for (var i in a_sorted) {
|
|
|
|
if (!a_sorted[i].equals(b_sorted[i])) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates string representation of the Source List.
|
|
|
|
* Should be fairly similar to the original.
|
|
|
|
*/
|
|
|
|
toString:
|
|
|
|
function() {
|
|
|
|
if (this.isNone()) {
|
|
|
|
return "'none'";
|
|
|
|
}
|
|
|
|
if (this._permitAllSources) {
|
|
|
|
return "*";
|
|
|
|
}
|
|
|
|
return this._sources.map(function(x) { return x.toString(); }).join(" ");
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether or not this source list represents the "'none'" special
|
|
|
|
* case.
|
|
|
|
*/
|
|
|
|
isNone:
|
|
|
|
function() {
|
|
|
|
return (!this._permitAllSources) && (this._sources.length < 1);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether or not this source list permits all sources (*).
|
|
|
|
*/
|
|
|
|
isAll:
|
|
|
|
function() {
|
|
|
|
return this._permitAllSources;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Makes a new instance that resembles this object.
|
|
|
|
* @returns
|
|
|
|
* a new CSPSourceList
|
|
|
|
*/
|
|
|
|
clone:
|
|
|
|
function() {
|
|
|
|
var aSL = new CSPSourceList();
|
|
|
|
aSL._permitAllSources = this._permitAllSources;
|
|
|
|
for (var i in this._sources) {
|
|
|
|
aSL._sources[i] = this._sources[i].clone();
|
|
|
|
}
|
|
|
|
return aSL;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if this directive accepts a URI.
|
|
|
|
* @param aURI
|
|
|
|
* the URI in question
|
|
|
|
* @returns
|
|
|
|
* true if the URI matches a source in this source list.
|
|
|
|
*/
|
|
|
|
permits:
|
|
|
|
function cspsd_permits(aURI) {
|
|
|
|
if (this.isNone()) return false;
|
|
|
|
if (this.isAll()) return true;
|
|
|
|
|
|
|
|
for (var i in this._sources) {
|
|
|
|
if (this._sources[i].permits(aURI)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Intersects with another CSPSourceList, deciding the subset directive
|
|
|
|
* that should be enforced, and returning a new instance.
|
|
|
|
* @param that
|
|
|
|
* the other CSPSourceList to intersect "this" with
|
|
|
|
* @returns
|
|
|
|
* a new instance of a CSPSourceList representing the intersection
|
|
|
|
*/
|
|
|
|
intersectWith:
|
|
|
|
function cspsd_intersectWith(that) {
|
|
|
|
|
|
|
|
var newCSPSrcList = null;
|
|
|
|
|
|
|
|
if (this.isNone() || that.isNone())
|
|
|
|
newCSPSrcList = CSPSourceList.fromString("'none'");
|
|
|
|
|
|
|
|
if (this.isAll()) newCSPSrcList = that.clone();
|
|
|
|
if (that.isAll()) newCSPSrcList = this.clone();
|
|
|
|
|
|
|
|
if (!newCSPSrcList) {
|
|
|
|
// the shortcuts didn't apply, must do intersection the hard way.
|
|
|
|
// -- find only common sources
|
|
|
|
|
|
|
|
// XXX (sid): we should figure out a better algorithm for this.
|
|
|
|
// This is horribly inefficient.
|
|
|
|
var isrcs = [];
|
|
|
|
for (var i in this._sources) {
|
|
|
|
for (var j in that._sources) {
|
|
|
|
var s = that._sources[j].intersectWith(this._sources[i]);
|
|
|
|
if (s) {
|
|
|
|
isrcs.push(s);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Next, remove duplicates
|
|
|
|
dup: for (var i = 0; i < isrcs.length; i++) {
|
|
|
|
for (var j = 0; j < i; j++) {
|
|
|
|
if (isrcs[i].equals(isrcs[j])) {
|
|
|
|
isrcs.splice(i, 1);
|
|
|
|
i--;
|
|
|
|
continue dup;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newCSPSrcList = new CSPSourceList();
|
|
|
|
newCSPSrcList._sources = isrcs;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if either was explicit, so is this.
|
|
|
|
newCSPSrcList._isImplicit = this._isImplicit && that._isImplicit;
|
|
|
|
|
|
|
|
return newCSPSrcList;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
|
|
* Class to model a source (scheme, host, port)
|
|
|
|
*/
|
|
|
|
function CSPSource() {
|
|
|
|
this._scheme = undefined;
|
|
|
|
this._port = undefined;
|
|
|
|
this._host = undefined;
|
|
|
|
|
|
|
|
// when set to true, this source represents 'self'
|
|
|
|
this._isSelf = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* General factory method to create a new source from one of the following
|
|
|
|
* types:
|
|
|
|
* - nsURI
|
|
|
|
* - string
|
|
|
|
* - CSPSource (clone)
|
|
|
|
*/
|
|
|
|
CSPSource.create = function(aData, self, enforceSelfChecks) {
|
|
|
|
if (typeof aData === 'string')
|
|
|
|
return CSPSource.fromString(aData, self, enforceSelfChecks);
|
|
|
|
|
|
|
|
if (aData instanceof Components.interfaces.nsIURI)
|
|
|
|
return CSPSource.fromURI(aData, self, enforceSelfChecks);
|
|
|
|
|
|
|
|
if (aData instanceof CSPSource) {
|
|
|
|
var ns = aData.clone();
|
|
|
|
ns._self = CSPSource.create(self);
|
|
|
|
return ns;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Factory to create a new CSPSource, from a nsIURI.
|
|
|
|
*
|
|
|
|
* Don't use this if you want to wildcard ports!
|
|
|
|
*
|
|
|
|
* @param aURI
|
|
|
|
* nsIURI rep of a URI
|
|
|
|
* @param self (optional)
|
|
|
|
* string or CSPSource representing the "self" source
|
|
|
|
* @param enforceSelfChecks (optional)
|
|
|
|
* if present, and "true", will check to be sure "self" has the
|
|
|
|
* appropriate values to inherit when they are omitted from aURI.
|
|
|
|
* @returns
|
|
|
|
* an instance of CSPSource
|
|
|
|
*/
|
|
|
|
CSPSource.fromURI = function(aURI, self, enforceSelfChecks) {
|
|
|
|
if (!(aURI instanceof Components.interfaces.nsIURI)){
|
|
|
|
CSPError("Provided argument is not an nsIURI");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!self && enforceSelfChecks) {
|
|
|
|
CSPError("Can't use 'self' if self data is not provided");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (self && !(self instanceof CSPSource)) {
|
|
|
|
self = CSPSource.create(self, undefined, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
var sObj = new CSPSource();
|
|
|
|
sObj._self = self;
|
|
|
|
|
|
|
|
// PARSE
|
|
|
|
// If 'self' is undefined, then use default port for scheme if there is one.
|
|
|
|
|
|
|
|
// grab scheme (if there is one)
|
|
|
|
try {
|
|
|
|
sObj._scheme = aURI.scheme;
|
|
|
|
} catch(e) {
|
|
|
|
sObj._scheme = undefined;
|
|
|
|
CSPError("can't parse a URI without a scheme: " + aURI.asciiSpec);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// grab host (if there is one)
|
|
|
|
try {
|
|
|
|
// if there's no host, an exception will get thrown
|
|
|
|
// (NS_ERROR_FAILURE)
|
|
|
|
sObj._host = CSPHost.fromString(aURI.host);
|
|
|
|
} catch(e) {
|
|
|
|
sObj._host = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
// grab port (if there is one)
|
|
|
|
// creating a source from an nsURI is limited in that one cannot specify "*"
|
|
|
|
// for port. In fact, there's no way to represent "*" differently than
|
|
|
|
// a blank port in an nsURI, since "*" turns into -1, and so does an
|
|
|
|
// absence of port declaration.
|
|
|
|
try {
|
|
|
|
// if there's no port, an exception will get thrown
|
|
|
|
// (NS_ERROR_FAILURE)
|
|
|
|
if (aURI.port > 0) {
|
|
|
|
sObj._port = aURI.port;
|
|
|
|
} else {
|
|
|
|
// port is never inherited from self -- this gets too confusing.
|
|
|
|
// Instead, whatever scheme is used (an explicit one or the inherited
|
|
|
|
// one) dictates the port if no port is explicitly stated.
|
|
|
|
if (sObj._scheme) {
|
|
|
|
sObj._port = gIoService.getProtocolHandler(sObj._scheme).defaultPort;
|
|
|
|
if (sObj._port < 1)
|
|
|
|
sObj._port = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch(e) {
|
|
|
|
sObj._port = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return sObj;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Factory to create a new CSPSource, parsed from a string.
|
|
|
|
*
|
|
|
|
* @param aStr
|
|
|
|
* string rep of a CSP Source
|
|
|
|
* @param self (optional)
|
|
|
|
* string or CSPSource representing the "self" source
|
|
|
|
* @param enforceSelfChecks (optional)
|
|
|
|
* if present, and "true", will check to be sure "self" has the
|
|
|
|
* appropriate values to inherit when they are omitted from aURI.
|
|
|
|
* @returns
|
|
|
|
* an instance of CSPSource
|
|
|
|
*/
|
|
|
|
CSPSource.fromString = function(aStr, self, enforceSelfChecks) {
|
|
|
|
if (!aStr)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
if (!(typeof aStr === 'string')) {
|
|
|
|
CSPError("Provided argument is not a string");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!self && enforceSelfChecks) {
|
|
|
|
CSPError("Can't use 'self' if self data is not provided");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (self && !(self instanceof CSPSource)) {
|
|
|
|
self = CSPSource.create(self, undefined, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
var sObj = new CSPSource();
|
|
|
|
sObj._self = self;
|
|
|
|
|
|
|
|
// take care of 'self' keyword
|
|
|
|
if (aStr === "'self'") {
|
|
|
|
if (!self) {
|
|
|
|
CSPError("self keyword used, but no self data specified");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
sObj._isSelf = true;
|
|
|
|
sObj._self = self.clone();
|
|
|
|
return sObj;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We could just create a URI and then send this off to fromURI, but
|
|
|
|
// there's no way to leave out the scheme or wildcard the port in an nsURI.
|
|
|
|
// That has to be supported here.
|
|
|
|
|
|
|
|
// split it up
|
|
|
|
var chunks = aStr.split(":");
|
|
|
|
|
|
|
|
// If there is only one chunk, it's gotta be a host.
|
|
|
|
if (chunks.length == 1) {
|
|
|
|
sObj._host = CSPHost.fromString(chunks[0]);
|
|
|
|
if (!sObj._host) {
|
|
|
|
CSPError("Couldn't parse invalid source " + aStr);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// enforce 'self' inheritance
|
|
|
|
if (enforceSelfChecks) {
|
|
|
|
// note: the non _scheme accessor checks sObj._self
|
|
|
|
if (!sObj.scheme || !sObj.port) {
|
|
|
|
CSPError("Can't create host-only source " + aStr + " without 'self' data");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return sObj;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there are two chunks, it's either scheme://host or host:port
|
|
|
|
// ... but scheme://host can have an empty host.
|
|
|
|
// ... and host:port can have an empty host
|
|
|
|
if (chunks.length == 2) {
|
|
|
|
|
|
|
|
// is the last bit a port?
|
|
|
|
if (chunks[1] === "*" || chunks[1].match(/^\d+$/)) {
|
|
|
|
sObj._port = chunks[1];
|
|
|
|
// then the previous chunk *must* be a host or empty.
|
|
|
|
if (chunks[0] !== "") {
|
|
|
|
sObj._host = CSPHost.fromString(chunks[0]);
|
|
|
|
if (!sObj._host) {
|
|
|
|
CSPError("Couldn't parse invalid source " + aStr);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// enforce 'self' inheritance
|
|
|
|
// (scheme:host requires port, host:port does too. Wildcard support is
|
|
|
|
// only available if the scheme and host are wildcarded)
|
|
|
|
if (enforceSelfChecks) {
|
|
|
|
// note: the non _scheme accessor checks sObj._self
|
|
|
|
if (!sObj.scheme || !sObj.host || !sObj.port) {
|
|
|
|
CSPError("Can't create source " + aStr + " without 'self' data");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// is the first bit a scheme?
|
|
|
|
else if (CSPSource.validSchemeName(chunks[0])) {
|
|
|
|
sObj._scheme = chunks[0];
|
|
|
|
// then the second bit *must* be a host or empty
|
|
|
|
if (chunks[1] === "") {
|
|
|
|
// Allow scheme-only sources! These default to wildcard host/port,
|
|
|
|
// especially since host and port don't always matter.
|
|
|
|
// Example: "javascript:" and "data:"
|
|
|
|
if (!sObj._host) sObj._host = "*";
|
|
|
|
if (!sObj._port) sObj._port = "*";
|
|
|
|
} else {
|
|
|
|
// some host was defined.
|
|
|
|
// ... remove <= 3 leading slashes (from the scheme) and parse
|
|
|
|
var cleanHost = chunks[1].replace(/^\/{0,3}/,"");
|
|
|
|
// ... and parse
|
|
|
|
sObj._host = CSPHost.fromString(cleanHost);
|
|
|
|
if (!sObj._host) {
|
|
|
|
CSPError("Couldn't parse invalid host " + cleanHost);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// enforce 'self' inheritance (scheme-only should be scheme:*:* now, and
|
|
|
|
// if there was a host provided it should be scheme:host:selfport
|
|
|
|
if (enforceSelfChecks) {
|
|
|
|
// note: the non _scheme accessor checks sObj._self
|
|
|
|
if (!sObj.scheme || !sObj.host || !sObj.port) {
|
|
|
|
CSPError("Can't create source " + aStr + " without 'self' data");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// AAAH! Don't know what to do! No valid scheme or port!
|
|
|
|
CSPError("Couldn't parse invalid source " + aStr);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return sObj;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there are three chunks, we got 'em all!
|
|
|
|
if (!CSPSource.validSchemeName(chunks[0])) {
|
|
|
|
CSPError("Couldn't parse scheme in " + aStr);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
sObj._scheme = chunks[0];
|
|
|
|
if (!(chunks[2] === "*" || chunks[2].match(/^\d+$/))) {
|
|
|
|
CSPError("Couldn't parse port in " + aStr);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
sObj._port = chunks[2];
|
|
|
|
|
|
|
|
// ... remove <= 3 leading slashes (from the scheme) and parse
|
|
|
|
var cleanHost = chunks[1].replace(/^\/{0,3}/,"");
|
|
|
|
sObj._host = CSPHost.fromString(cleanHost);
|
|
|
|
|
|
|
|
return sObj._host ? sObj : null;
|
|
|
|
};
|
|
|
|
|
|
|
|
CSPSource.validSchemeName = function(aStr) {
|
|
|
|
// <scheme-name> ::= <alpha><scheme-suffix>
|
|
|
|
// <scheme-suffix> ::= <scheme-chr>
|
|
|
|
// | <scheme-suffix><scheme-chr>
|
|
|
|
// <scheme-chr> ::= <letter> | <digit> | "+" | "." | "-"
|
|
|
|
|
|
|
|
return aStr.match(/^[a-zA-Z][a-zA-Z0-9+.-]*$/);
|
|
|
|
};
|
|
|
|
|
|
|
|
CSPSource.prototype = {
|
|
|
|
|
|
|
|
get scheme () {
|
|
|
|
if (!this._scheme && this._self)
|
|
|
|
return this._self.scheme;
|
|
|
|
return this._scheme;
|
|
|
|
},
|
|
|
|
|
|
|
|
get host () {
|
|
|
|
if (!this._host && this._self)
|
|
|
|
return this._self.host;
|
|
|
|
return this._host;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If 'self' has port hard-defined, and this doesn't have a port
|
|
|
|
* hard-defined, use the self's port. Otherwise, if both are implicit,
|
|
|
|
* resolve default port for this scheme.
|
|
|
|
*/
|
|
|
|
get port () {
|
|
|
|
if (this._port) return this._port;
|
|
|
|
// if no port, get the default port for the scheme.
|
|
|
|
if (this._scheme) {
|
|
|
|
try {
|
|
|
|
var port = gIoService.getProtocolHandler(this._scheme).defaultPort;
|
|
|
|
if (port > 0) return port;
|
|
|
|
} catch(e) {
|
|
|
|
// if any errors happen, fail gracefully.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// if there was no scheme (and thus no default scheme), return self.port
|
|
|
|
if (this._self && this._self.port) return this._self.port;
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates string representation of the Source.
|
|
|
|
* Should be fairly similar to the original.
|
|
|
|
*/
|
|
|
|
toString:
|
|
|
|
function() {
|
|
|
|
if (this._isSelf)
|
|
|
|
return this._self.toString();
|
|
|
|
|
|
|
|
var s = "";
|
|
|
|
if (this._scheme)
|
|
|
|
s = s + this._scheme + "://";
|
|
|
|
if (this._host)
|
|
|
|
s = s + this._host;
|
|
|
|
if (this._port)
|
|
|
|
s = s + ":" + this._port;
|
|
|
|
return s;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Makes a new instance that resembles this object.
|
|
|
|
* @returns
|
|
|
|
* a new CSPSource
|
|
|
|
*/
|
|
|
|
clone:
|
|
|
|
function() {
|
|
|
|
var aClone = new CSPSource();
|
|
|
|
aClone._self = this._self ? this._self.clone() : undefined;
|
|
|
|
aClone._scheme = this._scheme;
|
|
|
|
aClone._port = this._port;
|
|
|
|
aClone._host = this._host ? this._host.clone() : undefined;
|
|
|
|
aClone._isSelf = this._isSelf;
|
|
|
|
return aClone;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if this Source accepts a URI.
|
|
|
|
* @param aSource
|
|
|
|
* the URI, or CSPSource in question
|
|
|
|
* @returns
|
|
|
|
* true if the URI matches a source in this source list.
|
|
|
|
*/
|
|
|
|
permits:
|
|
|
|
function(aSource) {
|
|
|
|
if (!aSource) return false;
|
|
|
|
|
|
|
|
if (!(aSource instanceof CSPSource))
|
|
|
|
return this.permits(CSPSource.create(aSource));
|
|
|
|
|
|
|
|
// verify scheme
|
|
|
|
if (this.scheme != aSource.scheme)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// port is defined in 'this' (undefined means it may not be relevant
|
|
|
|
// to the scheme) AND this port (implicit or explicit) matches
|
|
|
|
// aSource's port
|
|
|
|
if (this.port && this.port !== "*" && this.port != aSource.port)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// host is defined in 'this' (undefined means it may not be relevant
|
|
|
|
// to the scheme) AND this host (implicit or explicit) permits
|
|
|
|
// aSource's host.
|
|
|
|
if (this.host && !this.host.permits(aSource.host))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// all scheme, host and port matched!
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines the intersection of two sources.
|
|
|
|
* Returns a null object if intersection generates no
|
|
|
|
* hosts that satisfy it.
|
|
|
|
* @param that
|
|
|
|
* the other CSPSource to intersect "this" with
|
|
|
|
* @returns
|
|
|
|
* a new instance of a CSPSource representing the intersection
|
|
|
|
*/
|
|
|
|
intersectWith:
|
|
|
|
function(that) {
|
|
|
|
var newSource = new CSPSource();
|
|
|
|
|
|
|
|
// 'self' is not part of the intersection. Intersect the raw values from
|
|
|
|
// the source, self must be set by someone creating this source.
|
|
|
|
// When intersecting, we take the more specific of the two: if one scheme,
|
|
|
|
// host or port is undefined, the other is taken. (This is contrary to
|
|
|
|
// when "permits" is called -- there, the value of 'self' is looked at
|
|
|
|
// when a scheme, host or port is undefined.)
|
|
|
|
|
|
|
|
// port
|
|
|
|
if (!this._port)
|
|
|
|
newSource._port = that._port;
|
|
|
|
else if (!that._port)
|
|
|
|
newSource._port = this._port;
|
|
|
|
else if (this._port === "*")
|
|
|
|
newSource._port = that._port;
|
|
|
|
else if (that._port === "*")
|
|
|
|
newSource._port = this._port;
|
|
|
|
else if (that._port === this._port)
|
|
|
|
newSource._port = this._port;
|
|
|
|
else {
|
|
|
|
CSPError("Could not intersect " + this + " with " + that
|
|
|
|
+ " due to port problems.");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// scheme
|
|
|
|
if (!this._scheme)
|
|
|
|
newSource._scheme = that._scheme;
|
|
|
|
else if (!that._scheme)
|
|
|
|
newSource._scheme = this._scheme;
|
|
|
|
if (this._scheme === "*")
|
|
|
|
newSource._scheme = that._scheme;
|
|
|
|
else if (that._scheme === "*")
|
|
|
|
newSource._scheme = this._scheme;
|
|
|
|
else if (that._scheme === this._scheme)
|
|
|
|
newSource._scheme = this._scheme;
|
|
|
|
else {
|
|
|
|
CSPError("Could not intersect " + this + " with " + that
|
|
|
|
+ " due to scheme problems.");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// host
|
|
|
|
if (!this._host)
|
|
|
|
newSource._host = that._host;
|
|
|
|
else if (!that._host)
|
|
|
|
newSource._host = this._host;
|
|
|
|
else // both this and that have hosts
|
|
|
|
newSource._host = this._host.intersectWith(that._host);
|
|
|
|
|
|
|
|
return newSource;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Compares one CSPSource to another.
|
|
|
|
*
|
|
|
|
* @param that
|
|
|
|
* another CSPSource
|
|
|
|
* @param resolveSelf (optional)
|
|
|
|
* if present, and 'true', implied values are obtained from 'self'
|
|
|
|
* instead of assumed to be "anything"
|
|
|
|
* @returns
|
|
|
|
* true if they have the same data
|
|
|
|
*/
|
|
|
|
equals:
|
|
|
|
function(that, resolveSelf) {
|
|
|
|
// 1. schemes match
|
|
|
|
// 2. ports match
|
|
|
|
// 3. either both hosts are undefined, or one equals the other.
|
|
|
|
if (resolveSelf)
|
|
|
|
return this.scheme === that.scheme
|
|
|
|
&& this.port === that.port
|
|
|
|
&& (!(this.host || that.host) ||
|
|
|
|
(this.host && this.host.equals(that.host)));
|
|
|
|
|
|
|
|
// otherwise, compare raw (non-self-resolved values)
|
|
|
|
return this._scheme === that._scheme
|
|
|
|
&& this._port === that._port
|
|
|
|
&& (!(this._host || that._host) ||
|
|
|
|
(this._host && this._host.equals(that._host)));
|
|
|
|
},
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
|
|
* Class to model a host *.x.y.
|
|
|
|
*/
|
|
|
|
function CSPHost() {
|
|
|
|
this._segments = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Factory to create a new CSPHost, parsed from a string.
|
|
|
|
*
|
|
|
|
* @param aStr
|
|
|
|
* string rep of a CSP Host
|
|
|
|
* @returns
|
|
|
|
* an instance of CSPHost
|
|
|
|
*/
|
|
|
|
CSPHost.fromString = function(aStr) {
|
|
|
|
if (!aStr) return null;
|
|
|
|
|
|
|
|
// host string must be LDH with dots and stars.
|
|
|
|
var invalidChar = aStr.match(/[^a-zA-Z0-9\-\.\*]/);
|
|
|
|
if (invalidChar) {
|
|
|
|
CSPdebug("Invalid character '" + invalidChar + "' in host " + aStr);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
var hObj = new CSPHost();
|
|
|
|
hObj._segments = aStr.split(/\./);
|
2010-04-23 12:57:51 -07:00
|
|
|
if (hObj._segments.length < 1)
|
2010-01-21 10:41:24 -08:00
|
|
|
return null;
|
|
|
|
|
|
|
|
// validate data in segments
|
|
|
|
for (var i in hObj._segments) {
|
|
|
|
var seg = hObj._segments[i];
|
|
|
|
if (seg == "*") {
|
|
|
|
if (i > 0) {
|
|
|
|
// Wildcard must be FIRST
|
|
|
|
CSPdebug("Wildcard char located at invalid position in '" + aStr + "'");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (seg.match(/[^a-zA-Z0-9\-]/)) {
|
|
|
|
// Non-wildcard segment must be LDH string
|
|
|
|
CSPdebug("Invalid segment '" + seg + "' in host value");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return hObj;
|
|
|
|
};
|
|
|
|
|
|
|
|
CSPHost.prototype = {
|
|
|
|
/**
|
|
|
|
* Generates string representation of the Source.
|
|
|
|
* Should be fairly similar to the original.
|
|
|
|
*/
|
|
|
|
toString:
|
|
|
|
function() {
|
|
|
|
return this._segments.join(".");
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Makes a new instance that resembles this object.
|
|
|
|
* @returns
|
|
|
|
* a new CSPHost
|
|
|
|
*/
|
|
|
|
clone:
|
|
|
|
function() {
|
|
|
|
var aHost = new CSPHost();
|
|
|
|
for (var i in this._segments) {
|
|
|
|
aHost._segments[i] = this._segments[i];
|
|
|
|
}
|
|
|
|
return aHost;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if this host accepts the provided host (or the other way
|
|
|
|
* around).
|
|
|
|
* @param aHost
|
|
|
|
* the FQDN in question (CSPHost or String)
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
permits:
|
|
|
|
function(aHost) {
|
|
|
|
if (!aHost) return false;
|
|
|
|
|
|
|
|
if (!(aHost instanceof CSPHost)) {
|
|
|
|
// -- compare CSPHost to String
|
|
|
|
return this.permits(CSPHost.fromString(aHost));
|
|
|
|
}
|
|
|
|
var thislen = this._segments.length;
|
|
|
|
var thatlen = aHost._segments.length;
|
|
|
|
|
|
|
|
// don't accept a less specific host:
|
|
|
|
// \--> *.b.a doesn't accept b.a.
|
|
|
|
if (thatlen < thislen) { return false; }
|
|
|
|
|
|
|
|
// check for more specific host (and wildcard):
|
|
|
|
// \--> *.b.a accepts d.c.b.a.
|
|
|
|
// \--> c.b.a doesn't accept d.c.b.a.
|
|
|
|
if ((thatlen > thislen) && this._segments[0] != "*") {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Given the wildcard condition (from above),
|
|
|
|
// only necessary to compare elements that are present
|
|
|
|
// in this host. Extra tokens in aHost are ok.
|
|
|
|
// * Compare from right to left.
|
|
|
|
for (var i=1; i <= thislen; i++) {
|
|
|
|
if (this._segments[thislen-i] != "*" &&
|
|
|
|
(this._segments[thislen-i] != aHost._segments[thatlen-i])) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// at this point, all conditions are met, so the host is allowed
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines the intersection of two Hosts.
|
|
|
|
* Basically, they must be the same, or one must have a wildcard.
|
|
|
|
* @param that
|
|
|
|
* the other CSPHost to intersect "this" with
|
|
|
|
* @returns
|
|
|
|
* a new instance of a CSPHost representing the intersection
|
|
|
|
* (or null, if they can't be intersected)
|
|
|
|
*/
|
|
|
|
intersectWith:
|
|
|
|
function(that) {
|
|
|
|
if (!(this.permits(that) || that.permits(this))) {
|
|
|
|
// host definitions cannot co-exist without a more general host
|
|
|
|
// ... one must be a subset of the other, or intersection makes no sense.
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// pick the more specific one, if both are same length.
|
|
|
|
if (this._segments.length == that._segments.length) {
|
|
|
|
// *.a vs b.a : b.a
|
|
|
|
return (this._segments[0] === "*") ? that.clone() : this.clone();
|
|
|
|
}
|
|
|
|
|
|
|
|
// different lengths...
|
|
|
|
// *.b.a vs *.a : *.b.a
|
|
|
|
// *.b.a vs d.c.b.a : d.c.b.a
|
|
|
|
return (this._segments.length > that._segments.length) ?
|
|
|
|
this.clone() : that.clone();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Compares one CSPHost to another.
|
|
|
|
*
|
|
|
|
* @param that
|
|
|
|
* another CSPHost
|
|
|
|
* @returns
|
|
|
|
* true if they have the same data
|
|
|
|
*/
|
|
|
|
equals:
|
|
|
|
function(that) {
|
|
|
|
if (this._segments.length != that._segments.length)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
for (var i=0; i<this._segments.length; i++) {
|
|
|
|
if (this._segments[i] != that._segments[i])
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
};
|