summaryrefslogtreecommitdiff
path: root/data/extensions/https-everywhere-eff@eff.org/chrome/content/code/HTTPSRules.js
blob: b3818bf8c68b96bdfaa6c361f6f25da59e1d47a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
// Compilation of RegExps is now delayed until they are first used...

function Rule(from, to) {
  this.to = to;
  this.from_c = from; // This will become a RegExp after compilation
}

function Exclusion(pattern) {
  this.pattern_c = pattern; // Will become a RegExp after compilation
}

function CookieRule(host, cookiename) {
  this.host = host;
  this.name = cookiename;

  // These will be made during compilation:

  //this.host_c = new RegExp(host);
  //this.name_c = new RegExp(cookiename);
}

function RuleSet(id, name, xmlName, match_rule, default_off, platform) {
  if(xmlName == "WordPress.xml" || xmlName == "Github.xml") {
    this.log(NOTE, "RuleSet( name="+name+", xmlName="+xmlName+", match_rule="+match_rule+", default_off="+default_off+", platform="+platform+" )");
  }

  this.id=id;
  this.on_by_default = true;
  this.compiled = false;
  this.name = name;
  this.xmlName = xmlName;
  this.notes = "";

  if (match_rule)   this.ruleset_match_c = new RegExp(match_rule);
  else              this.ruleset_match_c = null;
  if (default_off) {
    // Perhaps problematically, this currently ignores the actual content of
    // the default_off XML attribute.  Ideally we'd like this attribute to be
    // "valueless"
    this.notes = default_off;
    this.on_by_default = false;
  }
  if (platform)
    if (platform.search(HTTPSRules.localPlatformRegexp) == -1) {
      this.on_by_default = false;
      this.notes = "Only for " + platform;
    }

  this.rules = [];
  this.exclusions = [];
  this.cookierules = [];

  this.rule_toggle_prefs = HTTPSEverywhere.instance.rule_toggle_prefs;

  try {
    // if this pref exists, use it
    this.active = this.rule_toggle_prefs.getBoolPref(name);
  } catch(e) {
    // if not, use the default
    this.active = this.on_by_default;
  }
}

var dom_parser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);

RuleSet.prototype = {

  ensureCompiled: function() {
    // Postpone compilation of exclusions, rules and cookies until now, to accelerate
    // browser load time.
    if (this.compiled) return;
    var i;

    for (i = 0; i < this.exclusions.length; ++i) {
      this.exclusions[i].pattern_c = new RegExp(this.exclusions[i].pattern_c);
    }
    for (i = 0; i < this.rules.length; ++i) {
      this.rules[i].from_c = new RegExp(this.rules[i].from_c);
    }

    for (i = 0; i < this.cookierules.length; i++) {
       var cr = this.cookierules[i];
       cr.host_c = new RegExp(cr.host);
       cr.name_c = new RegExp(cr.name);
    }

    this.compiled = true;
  },

  apply: function(urispec) {
    // return null if it does not apply
    // and the new url if it does apply
    var i;
    var returl = null;
    this.ensureCompiled();
    // If a rulset has a match_rule and it fails, go no further
    if (this.ruleset_match_c && !this.ruleset_match_c.test(urispec)) {
      this.log(VERB, "ruleset_match_c excluded " + urispec);
      return null;
    }
    // Even so, if we're covered by an exclusion, go home
    for (i = 0; i < this.exclusions.length; ++i) {
      if (this.exclusions[i].pattern_c.test(urispec)) {
        this.log(DBUG,"excluded uri " + urispec);
        return null;
      }
    }
    // Okay, now find the first rule that triggers
    for (i = 0; i < this.rules.length; ++i) {
      // This is just for displaying inactive rules
      returl = urispec.replace(this.rules[i].from_c, this.rules[i].to);
      if (returl != urispec) {
        // we rewrote the uri
        this.log(DBUG, "Rewrote " + urispec + " -> " + returl + " using " + this.xmlName + ": " + this.rules[i].from_c + " -> " + this.rules[i].to);
        return returl;
      }
    }

    return null;
  },
  log: function(level, msg) {
    https_everywhereLog(level, msg);
  },
 
  wouldMatch: function(hypothetical_uri, alist) {
    // return true if this ruleset would match the uri, assuming it were http
    // used for judging moot / inactive rulesets
    // alist is optional
 
    // if the ruleset is already somewhere in this applicable list, we don't
    // care about hypothetical wouldMatch questions
    if (alist && (this.name in alist.all)) return false;
 
    this.log(DBUG,"Would " +this.name + " match " +hypothetical_uri.spec +
             "?  serial " + (alist && alist.serial));
     
    var uri = hypothetical_uri.clone();
    if (uri.scheme == "https") uri.scheme = "http";
    var urispec = uri.spec;

    this.ensureCompiled();

    if (this.ruleset_match_c && !this.ruleset_match_c.test(urispec)) 
      return false;

    for (var i = 0; i < this.exclusions.length; ++i) 
      if (this.exclusions[i].pattern_c.test(urispec)) return false;

    for (var i = 0; i < this.rules.length; ++i) 
      if (this.rules[i].from_c.test(urispec)) return true;
    return false;
  },

  transformURI: function(uri) {
    // If no rule applies, return null; if a rule would have applied but was
    // inactive, return 0; otherwise, return a fresh uri instance
    // for the target
    var newurl = this.apply(uri.spec);
    if (null == newurl) 
      return null;
    var newuri = Components.classes["@mozilla.org/network/standard-url;1"].
                 createInstance(CI.nsIStandardURL);
    newuri.init(CI.nsIStandardURL.URLTYPE_STANDARD, 80,
             newurl, uri.originCharset, null);
    newuri = newuri.QueryInterface(CI.nsIURI);
    return newuri;
  },

  enable: function() {
    // Enable us.
    this.rule_toggle_prefs.setBoolPref(this.name, true);
    this.active = true;
  },

  disable: function() {
    // Disable us.
    this.rule_toggle_prefs.setBoolPref(this.name, false);
    this.active = false;
  },

  toggle: function() {
    this.active = !this.active;
    this.rule_toggle_prefs.setBoolPref(this.name, this.active);
  },

  clear: function() {
    try {
      this.rule_toggle_prefs.clearUserPref(this.name);
    } catch(e) {
      // this ruleset has never been toggled
    }
    this.active = this.on_by_default;
  }
};

const RuleWriter = {

  getCustomRuleDir: function() {
    var loc = "ProfD";  // profile directory
    var file =
      CC["@mozilla.org/file/directory_service;1"]
      .getService(CI.nsIProperties)
      .get(loc, CI.nsILocalFile)
      .clone();
    file.append("HTTPSEverywhereUserRules");
    // Check for existence, if not, create.
    if (!file.exists()) {
      file.create(CI.nsIFile.DIRECTORY_TYPE, 0700);
    }
    if (!file.isDirectory()) {
      // XXX: Arg, death!
    }
    return file;
  },

  chromeToPath: function (aPath) {
    if (!aPath || !(/^chrome:/.test(aPath)))
       return; //not a chrome url

    var ios =
      CC['@mozilla.org/network/io-service;1']
      .getService(CI.nsIIOService);
    var uri = ios.newURI(aPath, "UTF-8", null);
    var cr =
      CC['@mozilla.org/chrome/chrome-registry;1']
      .getService(CI.nsIChromeRegistry);
    var rv = cr.convertChromeURL(uri).spec;

    if (/^file:/.test(rv))
      rv = this.urlToPath(rv);
    else
      rv = this.urlToPath("file://"+rv);

    return rv;
  },

  urlToPath: function (aPath) {
    if (!aPath || !/^file:/.test(aPath))
      return ;

    var ph =
      CC["@mozilla.org/network/protocol;1?name=file"]
      .createInstance(CI.nsIFileProtocolHandler);
    var rv = ph.getFileFromURLSpec(aPath).path;

    return rv;
  },

  readFromUrl: function (url) {
    var ios = CC['@mozilla.org/network/io-service;1']
        .getService(CI.nsIIOService);
    var encoding = "UTF-8";
    var channel = ios.newChannel(url, encoding, null);
    var stream = channel.open();
    var streamSize = stream.available();

    if (!streamSize) {
      return null;
    }

    var convStream = CC["@mozilla.org/intl/converter-input-stream;1"]
        .createInstance(CI.nsIConverterInputStream);

    convStream.init(stream, encoding, streamSize,
        convStream.DEFAULT_REPLACEMENT_CHARACTER);

    var data = {};
    convStream.readString(streamSize, data);

    return data.value;
  },

  readFromFile: function(file) {
    if (!file.exists())
      return null;
    var data = "";
    var fstream = CC["@mozilla.org/network/file-input-stream;1"]
        .createInstance(CI.nsIFileInputStream);
    var sstream = CC["@mozilla.org/scriptableinputstream;1"]
        .createInstance(CI.nsIScriptableInputStream);
    fstream.init(file, -1, 0, 0);
    sstream.init(fstream);

    var str = sstream.read(4096);
    while (str.length > 0) {
      data += str;
      str = sstream.read(4096);
    }

    sstream.close();
    fstream.close();
    return data;
  },

  write: function(file, data) {
    //if (!file.exists())
    //  return null;
    this.log(DBUG, "Opening " + file.path + " for writing");
    var fstream = CC["@mozilla.org/network/file-output-stream;1"]
        .createInstance(CI.nsIFileOutputStream);
    fstream.init(file, -1, -1, 0);

    var retval = fstream.write(data, data.length);
    this.log(DBUG, "Got retval " + retval);
    fstream.close();
    return data;
  },

  rulesetFromFile: function(file, rule_store, ruleset_id) {
    if ((rule_store.targets == null) && (rule_store.targets != {}))
      this.log(WARN, "TARGETS IS NULL");
    var data = this.readFromFile(file);
    if (!data) return null;
    return this.readFromString(data, rule_store, ruleset_id);
  },

  readFromString: function(data, rule_store, ruleset_id) {
    try {
      var xmlruleset = dom_parser.parseFromString(data, "text/xml");
    } catch(e) { // file has been corrupted; XXX: handle error differently
      this.log(WARN,"Error in XML data: " + e + "\n" + data);
      return null;
    }
    this.parseOneRuleset(xmlruleset.documentElement, rule_store, ruleset_id);
  },

  parseOneRuleset: function(xmlruleset, rule_store, ruleset_id) {
    // Extract an xmlruleset into the rulestore
    if (!xmlruleset.getAttribute("name")) {
      this.log(WARN, "This blob: '" + xmlruleset + "' is not a ruleset\n");
      return null;
    }

    this.log(DBUG, "Parsing " + xmlruleset.getAttribute("name"));

    var match_rl = xmlruleset.getAttribute("match_rule");
    var dflt_off = xmlruleset.getAttribute("default_off");
    var platform = xmlruleset.getAttribute("platform");
    var rs = new RuleSet(ruleset_id, xmlruleset.getAttribute("name"), xmlruleset.getAttribute("f"), match_rl, dflt_off, platform);

    // see if this ruleset has the same name as an existing ruleset;
    // if so, this ruleset is ignored; DON'T add or return it.
    if (rs.name in rule_store.rulesetsByName) {
      this.log(WARN, "Error: found duplicate rule name " + rs.name);
      return null;
    }

    // Add this ruleset id into HTTPSRules.targets if it's not already there.
    // This should only happen for custom user rules. Built-in rules get
    // their ids preloaded into the targets map, and have their <target>
    // tags stripped when the JSON database is built.
    var targets = xmlruleset.getElementsByTagName("target");
    for (var i = 0; i < targets.length; i++) {
      var host = targets[i].getAttribute("host");
      if (!host) {
        this.log(WARN, "<target> missing host in " + xmlruleset.getAttribute("name"));
        return null;
      }
      if (! rule_store.targets[host])
        rule_store.targets[host] = [];
      this.log(DBUG, "Adding " + host + " to targets, pointing at " + ruleset_id);
      rule_store.targets[host].push(ruleset_id);
    }

    var exclusions = xmlruleset.getElementsByTagName("exclusion");
    for (var i = 0; i < exclusions.length; i++) {
      var exclusion = new Exclusion(exclusions[i].getAttribute("pattern"));
      rs.exclusions.push(exclusion);
    }

    var rules = xmlruleset.getElementsByTagName("rule");
    for (var i = 0; i < rules.length; i++) {
      var rule = new Rule(rules[i].getAttribute("from"),
                          rules[i].getAttribute("to"));
      rs.rules.push(rule);
    }

    var securecookies = xmlruleset.getElementsByTagName("securecookie");
    for (var i = 0; i < securecookies.length; i++) {
      var c_rule = new CookieRule(securecookies[i].getAttribute("host"),
                                  securecookies[i].getAttribute("name"));
      rs.cookierules.push(c_rule);
      this.log(DBUG,"Cookie rule "+ c_rule.host+ " " +c_rule.name);
    }

    rule_store.rulesets.push(rs);
    rule_store.rulesetsByID[rs.id] = rs;
    rule_store.rulesetsByName[rs.name] = rs;
  },

  enumerate: function(dir) {
    // file is the given directory (nsIFile)
    var entries = dir.directoryEntries;
    var ret = [];
    while(entries.hasMoreElements()) {
      var entry = entries.getNext();
      entry.QueryInterface(Components.interfaces.nsIFile);
      ret.push(entry);
    }
    return ret;
  },
};



const HTTPSRules = {
  init: function() {
    try {
      this.rulesets = [];
      this.targets = {};  // dict mapping target host pattern -> list of
                          // applicable ruleset ids
      this.rulesetsByID = {};
      this.rulesetsByName = {};
      var t1 = new Date().getTime();
      this.checkMixedContentHandling();
      var rulefiles = RuleWriter.enumerate(RuleWriter.getCustomRuleDir());

      this.loadTargets();
      this.scanRulefiles(rulefiles);

    } catch(e) {
      this.log(DBUG,"Rules Failed: "+e);
    }
    var t2 =  new Date().getTime();
    this.log(NOTE,"Loading targets took " + (t2 - t1) / 1000.0 + " seconds");

    return;
  },

  /**
   * Read and parse the ruleset JSON.
   * Note: This only parses the outer JSON wrapper. Each ruleset is itself an
   * XML string, which will be parsed on an as-needed basis.
   */
  loadTargets: function() {
    var loc = "chrome://https-everywhere/content/rulesets.json";
    var data = RuleWriter.readFromUrl(loc);
    var rules = JSON.parse(data);
    this.targets = rules.targets;
    this.rulesetStrings = rules.rulesetStrings;
  },

  checkMixedContentHandling: function() {
    // Firefox 23+ blocks mixed content by default, so rulesets that create
    // mixed content situations should be disabled there
    var appInfo = CC["@mozilla.org/xre/app-info;1"].getService(CI.nsIXULAppInfo);
    var platformVer = appInfo.platformVersion;
    var versionChecker = CC["@mozilla.org/xpcom/version-comparator;1"]
                          .getService(CI.nsIVersionComparator);
    var prefs = Components.classes["@mozilla.org/preferences-service;1"]
                        .getService(Components.interfaces.nsIPrefService).getBranch("");


    // If mixed content is present and enabled, and the user hasn't opted to enable
    // mixed content triggering rules, leave them out. Otherwise add them in.
    if(versionChecker.compare(appInfo.version, "23.0a1") >= 0
            && prefs.getBoolPref("security.mixed_content.block_active_content")
            && !prefs.getBoolPref("extensions.https_everywhere.enable_mixed_rulesets")) {
      this.log(INFO, "Not activating rules that trigger mixed content errors.");
      this.localPlatformRegexp = new RegExp("firefox");
    } else {
      this.log(INFO, "Activating rules that would normally trigger mixed content");
      this.localPlatformRegexp = new RegExp("(firefox|mixedcontent)");
    }
  },

  scanRulefiles: function(rulefiles) {
    var i = 0;
    var r = null;
    for(i = 0; i < rulefiles.length; ++i) {
      try {
        this.log(DBUG,"Loading ruleset file: "+rulefiles[i].path);
        var ruleset_id = "custom_" + i;
        RuleWriter.rulesetFromFile(rulefiles[i], this, ruleset_id);
      } catch(e) {
        this.log(WARN, "Error in ruleset file: " + e);
        if (e.lineNumber)
          this.log(WARN, "(line number: " + e.lineNumber + ")");
      }
    }
  },

  resetRulesetsToDefaults: function() {
    // Callable from within the prefs UI and also for cleaning up buggy
    // configurations...
    for (var i in this.rulesets) {
      this.rulesets[i].clear();
    }
  },


  rewrittenURI: function(alist, input_uri) {
    // This function oversees the task of working out if a uri should be
    // rewritten, what it should be rewritten to, and recordkeeping of which
    // applicable rulesets are and aren't active.  Previously this returned
    // the new uri if there was a rewrite.  Now it returns a JS object with a
    // newuri attribute and an applied_ruleset attribute (or null if there's
    // no rewrite).
    var i = 0; 
    userpass_present = false; // Global so that sanitiseURI can tweak it.
                              // Why does JS have no tuples, again?
    var blob = {}; blob.newuri = null;
    if (!alist) this.log(DBUG, "No applicable list rewriting " + input_uri.spec);
    this.log(DBUG, "Processing " + input_uri.spec);

    var uri = this.sanitiseURI(input_uri);

    // Get the list of rulesets that target this host
    try {
      var rs = this.potentiallyApplicableRulesets(uri.host);
    } catch(e) {
      this.log(NOTE, 'Could not check applicable rules for '+uri.spec + '\n'+e);
      return null;
    }

    // ponder each potentially applicable ruleset, working out if it applies
    // and recording it as active/inactive/moot/breaking in the applicable list
    for (i = 0; i < rs.length; ++i) {
      if (!rs[i].active) {
        if (alist && rs[i].wouldMatch(uri, alist))
          alist.inactive_rule(rs[i]);
        continue;
      } 
      blob.newuri = rs[i].transformURI(uri);
      if (blob.newuri) {
        if (alist) {
          if (uri.spec in https_everywhere_blacklist) 
            alist.breaking_rule(rs[i]);
          else 
            alist.active_rule(rs[i]);
  }
        if (userpass_present) blob.newuri.userPass = input_uri.userPass;
        blob.applied_ruleset = rs[i];
        return blob;
      }
      if (uri.scheme == "https" && alist) {
        // we didn't rewrite but the rule applies to this domain and the
        // requests are going over https
        if (rs[i].wouldMatch(uri, alist)) alist.moot_rule(rs[i]);
        continue;
      } 
    }
    return null;
  },

  sanitiseURI: function(input_uri) {
    // Rulesets shouldn't try to parse usernames and passwords.  If we find
    // those, apply the ruleset without them (and then add them back later).
    // When .userPass is absent, sometimes it is false and sometimes trying
    // to read it raises an exception (probably depending on the URI type).
    var uri = input_uri;
    try {
      if (input_uri.userPass) {
        uri = input_uri.clone();
        userpass_present = true; // tweaking a global in our caller :(
        uri.userPass = null;
      } 
    } catch(e) {}

    // example.com.  is equivalent to example.com
    // example.com.. is invalid, but firefox would load it anyway
    try {
      if (uri.host)
        try {
          var h = uri.host;
          if (h.charAt(h.length - 1) == ".") {
            while (h.charAt(h.length - 1) == ".") 
              h = h.slice(0,-1);
            uri = uri.clone();
            uri.host = h;
          }
        } catch(e) {
          this.log(WARN, "Failed to normalise domain: ");
          try       {this.log(WARN, input_uri.host);}
          catch(e2) {this.log(WARN, "bang" + e + " & " + e2 + " & "+ input_uri);}
        }
    } catch(e3) {
      this.log(INFO, "uri.host is explosive!");
      try       { this.log(INFO, "(" + uri.spec + ")"); }  // happens for about: uris and so forth
      catch(e4) { this.log(WARN, "(and unprintable!!!!!!)"); }
    }
    return uri;
  },

  setInsert: function(intoList, fromList) {
    // Insert any elements from fromList into intoList, if they are not
    // already there.  fromList may be null.
    if (!fromList) return;
    for (var i = 0; i < fromList.length; i++)
      if (intoList.indexOf(fromList[i]) == -1)
        intoList.push(fromList[i]);
  },

  // Load a ruleset by numeric id, e.g. 234
  loadRulesetById: function(ruleset_id) {
    RuleWriter.readFromString(this.rulesetStrings[ruleset_id], this, ruleset_id);
  },

  // Get all rulesets matching a given target, lazy-loading from DB as necessary.
  rulesetsByTarget: function(target) {
    var rulesetIds = this.targets[target];

    var output = [];
    if (rulesetIds) {
      this.log(INFO, "For target " + target + ", found ids " + rulesetIds.toString());
      for (var i = 0; i < rulesetIds.length; i++) {
        var id = rulesetIds[i];
        if (!this.rulesetsByID[id]) {
          this.loadRulesetById(id);
        }
        if (this.rulesetsByID[id]) {
          output.push(this.rulesetsByID[id]);
        }
      }
    } else {
      this.log(DBUG, "For target " + target + ", found no ids in DB");
    }
    return output;
  },

  /**
   * Return a list of rulesets that declare targets matching a given hostname.
   * The returned rulesets include those that are disabled for various reasons.
   * This function is only defined for fully-qualified hostnames. Wildcards and
   * cookie-style domain attributes with a leading dot are not permitted.
   * @param host {string}
   * @return {Array.<RuleSet>}
   */
  potentiallyApplicableRulesets: function(host) {
    var i, tmp, t;
    var results = [];

    var attempt = function(target) {
      this.setInsert(results, this.rulesetsByTarget(target));
    }.bind(this);

    attempt(host);

    // Ensure host is well-formed (RFC 1035)
    if (host.indexOf("..") != -1 || host.length > 255) {
      this.log(WARN,"Malformed host passed to potentiallyApplicableRulesets: " + host);
      return null;
    }

    // replace each portion of the domain with a * in turn
    var segmented = host.split(".");
    for (i = 0; i < segmented.length; ++i) {
      tmp = segmented[i];
      segmented[i] = "*";
      t = segmented.join(".");
      segmented[i] = tmp;
      attempt(t);
    }
    // now eat away from the left, with *, so that for x.y.z.google.com we
    // check *.z.google.com and *.google.com (we did *.y.z.google.com above)
    for (i = 2; i <= segmented.length - 2; ++i) {
      t = "*." + segmented.slice(i,segmented.length).join(".");
      attempt(t);
    }
    this.log(DBUG,"Potentially applicable rules for " + host + ":");
    for (i = 0; i < results.length; ++i)
      this.log(DBUG, "  " + results[i].name);
    return results;
  },

  /**
   * If a cookie's domain attribute has a leading dot to indicate it should be
   * sent for all subdomains (".example.com"), return the actual host part (the
   * part after the dot).
   *
   * @param cookieDomain {string} A cookie domain to strip a leading dot from.
   * @return {string} a fully qualified hostname.
   */
  hostFromCookieDomain: function(cookieDomain) {
    if (cookieDomain.length > 0 && cookieDomain[0] == ".") {
      return cookieDomain.slice(1);
    } else {
      return cookieDomain;
    }
  },

  /**
   * Check to see if the Cookie object c meets any of our cookierule criteria
   * for being marked as secure.
   *
   * @param applicable_list {ApplicableList} an ApplicableList for record keeping
   * @param c {nsICookie2} The cookie we might secure.
   * @param known_https {boolean} True if the cookie appeared in an HTTPS request and
   *   so we know it is okay to mark it secure (assuming a cookierule matches it.
   *   TODO(jsha): Double-check that the code calling this actually does that.
   * @return {boolean} True if the cookie in question should have the 'secure'
   *   flag set to true.
   */
  shouldSecureCookie: function(applicable_list, c, known_https) {
    this.log(DBUG,"  rawhost: " + c.rawHost + " name: " + c.name + " host" + c.host);
    var i,j;
    // potentiallyApplicableRulesets is defined on hostnames not cookie-style
    // "domain" attributes, so we strip a leading dot before calling.
    var rs = this.potentiallyApplicableRulesets(this.hostFromCookieDomain(c.host));
    for (i = 0; i < rs.length; ++i) {
      var ruleset = rs[i];
      if (ruleset.active) {
        ruleset.ensureCompiled();
        // Never secure a cookie if this page might be HTTP
        if (!(known_https || this.safeToSecureCookie(c.rawHost))) {
          continue;
        }
        for (j = 0; j < ruleset.cookierules.length; j++) {
          var cr = ruleset.cookierules[j];
          if (cr.host_c.test(c.host) && cr.name_c.test(c.name)) {
            if (applicable_list) applicable_list.active_rule(ruleset);
            this.log(INFO,"Active cookie rule " + ruleset.name);
            return true;
          }
        }
        if (ruleset.cookierules.length > 0 && applicable_list) {
          applicable_list.moot_rule(ruleset);
        }
      } else if (ruleset.cookierules.length > 0) {
        if (applicable_list) {
          applicable_list.inactive_rule(ruleset);
        }
        this.log(INFO,"Inactive cookie rule " + ruleset.name);
      }
    }
    return false;
  },

  /**
   * Check if the domain might be being served over HTTP.  If so, it isn't
   * safe to secure a cookie!  We can't always know this for sure because
   * observing cookie-changed doesn't give us enough context to know the
   * full origin URI. In particular, if cookies are set from Javascript (as
   * opposed to HTTP/HTTPS responses), we don't know what page context that
   * Javascript ran in.

   * First, if there are any redirect loops on this domain, don't secure
   * cookies.  XXX This is not a very satisfactory heuristic.  Sometimes we
   * would want to secure the cookie anyway, because the URLs that loop are
   * not authenticated or not important.  Also by the time the loop has been
   * observed and the domain blacklisted, a cookie might already have been
   * flagged as secure.
   *
   * @param domain {string} The cookie's 'domain' attribute.
   * @return {boolean} True if it's safe to secure a cookie on that domain.
   */
  safeToSecureCookie: function(domain) {
    if (domain in https_blacklist_domains) {
      this.log(INFO, "cookies for " + domain + "blacklisted");
      return false;
    }

    // If we passed that test, make up a random URL on the domain, and see if
    // we would HTTPSify that.
    try {
      var nonce_path = "/" + Math.random().toString();
      nonce_path = nonce_path + nonce_path;
      var test_uri = "http://" + domain + nonce_path;
    } catch (e) {
      this.log(WARN, "explosion in safeToSecureCookie for " + domain + "\n" 
                      + "(" + e + ")");
      return false;
    }

    this.log(DBUG, "Testing securecookie applicability with " + test_uri);
    // potentiallyApplicableRulesets is defined on hostnames not cookie-style
    // "domain" attributes, so we strip a leading dot before calling.
    var rs = this.potentiallyApplicableRulesets(this.hostFromCookieDomain(domain));
    for (var i = 0; i < rs.length; ++i) {
      if (!rs[i].active) continue;
      var rewrite = rs[i].apply(test_uri);
      if (rewrite) {
        this.log(DBUG, "Safe to secure cookie for " + test_uri + ": " + rewrite);
        return true;
      }
    }
    this.log(DBUG, "Unsafe to secure cookie for " + test_uri);
    return false;
  }
};