content/base/src/CSPUtils.jsm
author Sid Stamm <sstamm@mozilla.com>
Fri, 23 Apr 2010 12:57:51 -0700
changeset 41214 426165b08c0c4b5d952d775c9e5981713b05881a
parent 37160 a6ce37b09cf51cce00745096ee5606706e239443
child 43401 ffceae16b1a76867453dd233b80b08f2b65c163d
permissions -rw-r--r--
Bug 524066 - make CSP support dotless hosts, r=dveditz, a=dholbert_sheriff

/* ***** 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) {
            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) {
          CSPWarning("couldn't parse report URI: " + dirvalue);
        }
      }
      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
        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:
  //    <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(/\./);
  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<this._segments.length; i++) {
      if (this._segments[i] != that._segments[i])
        return false;
    }
    return true;
  }
};