bug 800444 - disable HSTS preload list if firefox has not updated in 18 weeks r=bsmit...
[gecko.git] / security / manager / tools / getHSTSPreloadList.js
blob63f48b59828a8be52272d6823cf072159bcfe801
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 // <https://developer.mozilla.org/en/XPConnect/xpcshell/HOWTO>
6 // <https://bugzilla.mozilla.org/show_bug.cgi?id=546628>
7 const Cc = Components.classes;
8 const Ci = Components.interfaces;
9 const Cu = Components.utils;
10 const Cr = Components.results;
12 // Register resource://app/ URI
13 let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
14 let resHandler = ios.getProtocolHandler("resource")
15                  .QueryInterface(Ci.nsIResProtocolHandler);
16 let mozDir = Cc["@mozilla.org/file/directory_service;1"]
17              .getService(Ci.nsIProperties)
18              .get("CurProcD", Ci.nsILocalFile);
19 let mozDirURI = ios.newFileURI(mozDir);
20 resHandler.setSubstitution("app", mozDirURI);
22 Cu.import("resource://gre/modules/Services.jsm");
23 Cu.import("resource://gre/modules/FileUtils.jsm");
24 Cu.import("resource:///modules/XPCOMUtils.jsm");
26 const SOURCE = "https://src.chromium.org/viewvc/chrome/trunk/src/net/base/transport_security_state_static.json";
27 const OUTPUT = "nsSTSPreloadList.inc";
28 const ERROR_OUTPUT = "nsSTSPreloadList.errors";
29 const MINIMUM_REQUIRED_MAX_AGE = 60 * 60 * 24 * 7 * 18;
30 const HEADER = "/* This Source Code Form is subject to the terms of the Mozilla Public\n" +
31 " * License, v. 2.0. If a copy of the MPL was not distributed with this\n" +
32 " * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n" +
33 "\n" +
34 "/*****************************************************************************/\n" +
35 "/* This is an automatically generated file. If you're not                    */\n" +
36 "/* nsStrictTransportSecurityService.cpp, you shouldn't be #including it.     */\n" +
37 "/*****************************************************************************/\n" +
38 "\n" +
39 "#include \"mozilla/StandardInteger.h\"\n";
40 const PREFIX = "\n" +
41 "class nsSTSPreload\n" +
42 "{\n" +
43 "  public:\n" +
44 "    const char *mHost;\n" +
45 "    const bool mIncludeSubdomains;\n" +
46 "};\n" +
47 "\n" +
48 "static const nsSTSPreload kSTSPreloadList[] = {\n";
49 const POSTFIX =  "};\n";
51 function download() {
52   var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
53             .createInstance(Ci.nsIXMLHttpRequest);
54   req.open("GET", SOURCE, false); // doing the request synchronously
55   try {
56     req.send();
57   }
58   catch (e) {
59     throw "ERROR: problem downloading '" + SOURCE + "': " + e;
60   }
62   if (req.status != 200) {
63     throw "ERROR: problem downloading '" + SOURCE + "': status " + req.status;
64   }
66   // we have to filter out '//' comments
67   var result = req.responseText.replace(/\/\/[^\n]*\n/g, "");
68   var data = null;
69   try {
70     data = JSON.parse(result);
71   }
72   catch (e) {
73     throw "ERROR: could not parse data from '" + SOURCE + "': " + e;
74   }
75   return data;
78 function getHosts(rawdata) {
79   var hosts = [];
81   if (!rawdata || !rawdata.entries) {
82     throw "ERROR: source data not formatted correctly: 'entries' not found";
83   }
85   for (entry of rawdata.entries) {
86     if (entry.mode && entry.mode == "force-https") {
87       if (entry.name) {
88         hosts.push(entry);
89       } else {
90         throw "ERROR: entry not formatted correctly: no name found";
91       }
92     }
93   }
95   return hosts;
98 var gSTSService = Cc["@mozilla.org/stsservice;1"]
99                   .getService(Ci.nsIStrictTransportSecurityService);
101 function processStsHeader(hostname, header, status) {
102   var maxAge = { value: 0 };
103   var includeSubdomains = { value: false };
104   var error = "no error";
105   if (header != null) {
106     try {
107       var uri = Services.io.newURI("https://" + host.name, null, null);
108       gSTSService.processStsHeader(uri, header, maxAge, includeSubdomains);
109     }
110     catch (e) {
111       dump("ERROR: could not process header '" + header + "' from " + hostname +
112            ": " + e + "\n");
113       error = e;
114     }
115   }
116   else {
117     if (status == 0) {
118       error = "could not connect to host";
119     } else {
120       error = "did not receive HSTS header";
121     }
122   }
124   return { hostname: hostname,
125            maxAge: maxAge.value,
126            includeSubdomains: includeSubdomains.value,
127            error: error };
130 function RedirectStopper() {};
132 RedirectStopper.prototype = {
133   // nsIChannelEventSink
134   asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
135     throw Cr.NS_ERROR_ENTITY_CHANGED;
136   },
138   getInterface: function(iid) {
139     return this.QueryInterface(iid);
140   },
142   QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink])
145 function getHSTSStatus(host, resultList) {
146   var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
147             .createInstance(Ci.nsIXMLHttpRequest);
148   var inResultList = false;
149   var uri = "https://" + host.name + "/";
150   req.open("GET", uri, true);
151   req.channel.notificationCallbacks = new RedirectStopper();
152   req.onreadystatechange = function(event) {
153     if (!inResultList && req.readyState == 4) {
154       inResultList = true;
155       var header = req.getResponseHeader("strict-transport-security");
156       resultList.push(processStsHeader(host.name, header, req.status));
157     }
158   };
160   try {
161     req.send();
162   }
163   catch (e) {
164     dump("ERROR: exception making request to " + host.name + ": " + e + "\n");
165   }
168 function compareHSTSStatus(a, b) {
169   return (a.hostname > b.hostname ? 1 : (a.hostname < b.hostname ? -1 : 0));
172 function writeTo(string, fos) {
173   fos.write(string, string.length);
176 // Determines and returns a string representing a declaration of when this
177 // preload list should no longer be used.
178 // This is the current time plus MINIMUM_REQUIRED_MAX_AGE.
179 function getExpirationTimeString() {
180   var now = new Date();
181   var nowMillis = now.getTime();
182   // MINIMUM_REQUIRED_MAX_AGE is in seconds, so convert to milliseconds
183   var expirationMillis = nowMillis + (MINIMUM_REQUIRED_MAX_AGE * 1000);
184   var expirationMicros = expirationMillis * 1000;
185   return "const PRTime gPreloadListExpirationTime = INT64_C(" + expirationMicros + ");\n";
188 function output(sortedStatuses) {
189   try {
190     var file = FileUtils.getFile("CurWorkD", [OUTPUT]);
191     var errorFile = FileUtils.getFile("CurWorkD", [ERROR_OUTPUT]);
192     var fos = FileUtils.openSafeFileOutputStream(file);
193     var eos = FileUtils.openSafeFileOutputStream(errorFile);
194     writeTo(HEADER, fos);
195     writeTo(getExpirationTimeString(), fos);
196     writeTo(PREFIX, fos);
197     for (var status of hstsStatuses) {
198       if (status.maxAge >= MINIMUM_REQUIRED_MAX_AGE) {
199         writeTo("  { \"" + status.hostname + "\", " +
200                  (status.includeSubdomains ? "true" : "false") + " },\n", fos);
201         dump("INFO: " + status.hostname + " ON the preload list\n");
202       }
203       else {
204         dump("INFO: " + status.hostname + " NOT ON the preload list\n");
205         if (status.maxAge != 0) {
206           status.error = "max-age too low: " + status.maxAge;
207         }
208         writeTo(status.hostname + ": " + status.error + "\n", eos);
209       }
210     }
211     writeTo(POSTFIX, fos);
212     FileUtils.closeSafeFileOutputStream(fos);
213     FileUtils.closeSafeFileOutputStream(eos);
214   }
215   catch (e) {
216     dump("ERROR: problem writing output to '" + OUTPUT + "': " + e + "\n");
217   }
220 // The idea is the output list will be the same size as the input list
221 // when we've received all responses (or timed out).
222 // Since all events are processed on the main thread, and since event
223 // handlers are not preemptible, there shouldn't be any concurrency issues.
224 function waitForResponses(inputList, outputList) {
225   // From <https://developer.mozilla.org/en/XPConnect/xpcshell/HOWTO>
226   var threadManager = Cc["@mozilla.org/thread-manager;1"]
227                       .getService(Ci.nsIThreadManager);
228   var mainThread = threadManager.currentThread;
229   while (inputList.length != outputList.length) {
230     mainThread.processNextEvent(true);
231   }
232   while (mainThread.hasPendingEvents()) {
233     mainThread.processNextEvent(true);
234   }
237 // ****************************************************************************
238 // This is where the action happens:
239 // disable the current preload list so it won't interfere with requests we make
240 Services.prefs.setBoolPref("network.stricttransportsecurity.preloadlist", false);
241 // download and parse the raw json file from the Chromium source
242 var rawdata = download();
243 // get just the hosts with mode: "force-https"
244 var hosts = getHosts(rawdata);
245 // spin off a request to each host
246 var hstsStatuses = [];
247 for (var host of hosts) {
248   getHSTSStatus(host, hstsStatuses);
250 // wait for those responses to come back
251 waitForResponses(hosts, hstsStatuses);
252 // sort the hosts alphabetically
253 hstsStatuses.sort(compareHSTSStatus);
254 // write the results to a file (this is where we filter out hosts that we
255 // either couldn't connect to, didn't receive an HSTS header from, couldn't
256 // parse the header, or had a header with too short a max-age)
257 output(hstsStatuses);
258 // ****************************************************************************