/* ***** 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 * * 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) { if (gETLDService.getBaseDomain(uri) === gETLDService.getBaseDomain(selfUri)) { okUriStrings.push(uriStrings[i]); } else { CSPWarning("can't use report URI from non-matching eTLD+1: " + gETLDService.getBaseDomain(uri)); } } } catch(e) { 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; } } } 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 aCSPR.makeExplicit(); return aCSPR; }; 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) { return false; } for (var dir in SD) { var dirv = SD[dir]; if (dirv === SD.ALLOW) continue; if (!this._directives[dirv]) { // 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(); 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: // ::= // | "'none'" // ::= // | " " 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) { // ::= // ::= // | // ::= | | "+" | "." | "-" 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(/\./); if (hObj._segments.length < 1) 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