security/manager/tools/getHSTSPreloadList.js

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:fe9d0c5d4486
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/. */
4
5 // How to run this file:
6 // 1. [obtain firefox source code]
7 // 2. [build/obtain firefox binaries]
8 // 3. run `[path to]/run-mozilla.sh [path to]/xpcshell \
9 // [path to]/getHSTSPreloadlist.js \
10 // [absolute path to]/nsSTSPreloadlist.inc'
11
12 // <https://developer.mozilla.org/en/XPConnect/xpcshell/HOWTO>
13 // <https://bugzilla.mozilla.org/show_bug.cgi?id=546628>
14 const Cc = Components.classes;
15 const Ci = Components.interfaces;
16 const Cu = Components.utils;
17 const Cr = Components.results;
18
19 // Register resource://app/ URI
20 let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
21 let resHandler = ios.getProtocolHandler("resource")
22 .QueryInterface(Ci.nsIResProtocolHandler);
23 let mozDir = Cc["@mozilla.org/file/directory_service;1"]
24 .getService(Ci.nsIProperties)
25 .get("CurProcD", Ci.nsILocalFile);
26 let mozDirURI = ios.newFileURI(mozDir);
27 resHandler.setSubstitution("app", mozDirURI);
28
29 Cu.import("resource://gre/modules/Services.jsm");
30 Cu.import("resource://gre/modules/FileUtils.jsm");
31 Cu.import("resource:///modules/XPCOMUtils.jsm");
32
33 const SOURCE = "https://src.chromium.org/chrome/trunk/src/net/http/transport_security_state_static.json";
34 const OUTPUT = "nsSTSPreloadList.inc";
35 const ERROR_OUTPUT = "nsSTSPreloadList.errors";
36 const MINIMUM_REQUIRED_MAX_AGE = 60 * 60 * 24 * 7 * 18;
37 const MAX_CONCURRENT_REQUESTS = 5;
38 const MAX_RETRIES = 3;
39 const REQUEST_TIMEOUT = 30 * 1000;
40 const ERROR_NONE = "no error";
41 const ERROR_CONNECTING_TO_HOST = "could not connect to host";
42 const ERROR_NO_HSTS_HEADER = "did not receive HSTS header";
43 const ERROR_MAX_AGE_TOO_LOW = "max-age too low: ";
44 const HEADER = "/* This Source Code Form is subject to the terms of the Mozilla Public\n" +
45 " * License, v. 2.0. If a copy of the MPL was not distributed with this\n" +
46 " * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n" +
47 "\n" +
48 "/*****************************************************************************/\n" +
49 "/* This is an automatically generated file. If you're not */\n" +
50 "/* nsSiteSecurityService.cpp, you shouldn't be #including it. */\n" +
51 "/*****************************************************************************/\n" +
52 "\n" +
53 "#include <stdint.h>\n";
54 const PREFIX = "\n" +
55 "class nsSTSPreload\n" +
56 "{\n" +
57 " public:\n" +
58 " const char *mHost;\n" +
59 " const bool mIncludeSubdomains;\n" +
60 "};\n" +
61 "\n" +
62 "static const nsSTSPreload kSTSPreloadList[] = {\n";
63 const POSTFIX = "};\n";
64
65 function download() {
66 var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
67 .createInstance(Ci.nsIXMLHttpRequest);
68 req.open("GET", SOURCE, false); // doing the request synchronously
69 try {
70 req.send();
71 }
72 catch (e) {
73 throw "ERROR: problem downloading '" + SOURCE + "': " + e;
74 }
75
76 if (req.status != 200) {
77 throw "ERROR: problem downloading '" + SOURCE + "': status " + req.status;
78 }
79
80 // we have to filter out '//' comments
81 var result = req.responseText.replace(/\/\/[^\n]*\n/g, "");
82 var data = null;
83 try {
84 data = JSON.parse(result);
85 }
86 catch (e) {
87 throw "ERROR: could not parse data from '" + SOURCE + "': " + e;
88 }
89 return data;
90 }
91
92 function getHosts(rawdata) {
93 var hosts = [];
94
95 if (!rawdata || !rawdata.entries) {
96 throw "ERROR: source data not formatted correctly: 'entries' not found";
97 }
98
99 for (entry of rawdata.entries) {
100 if (entry.mode && entry.mode == "force-https") {
101 if (entry.name) {
102 entry.retries = MAX_RETRIES;
103 entry.originalIncludeSubdomains = entry.include_subdomains;
104 hosts.push(entry);
105 } else {
106 throw "ERROR: entry not formatted correctly: no name found";
107 }
108 }
109 }
110
111 return hosts;
112 }
113
114 var gSSService = Cc["@mozilla.org/ssservice;1"]
115 .getService(Ci.nsISiteSecurityService);
116
117 function processStsHeader(host, header, status) {
118 var maxAge = { value: 0 };
119 var includeSubdomains = { value: false };
120 var error = ERROR_NONE;
121 if (header != null) {
122 try {
123 var uri = Services.io.newURI("https://" + host.name, null, null);
124 gSSService.processHeader(Ci.nsISiteSecurityService.HEADER_HSTS,
125 uri, header, 0, maxAge, includeSubdomains);
126 }
127 catch (e) {
128 dump("ERROR: could not process header '" + header + "' from " +
129 host.name + ": " + e + "\n");
130 error = e;
131 }
132 }
133 else {
134 if (status == 0) {
135 error = ERROR_CONNECTING_TO_HOST;
136 } else {
137 error = ERROR_NO_HSTS_HEADER;
138 }
139 }
140
141 let forceInclude = (host.forceInclude || host.pins == "google");
142
143 if (error == ERROR_NONE && maxAge.value < MINIMUM_REQUIRED_MAX_AGE) {
144 error = ERROR_MAX_AGE_TOO_LOW;
145 }
146
147 return { name: host.name,
148 maxAge: maxAge.value,
149 includeSubdomains: includeSubdomains.value,
150 error: error,
151 retries: host.retries - 1,
152 forceInclude: forceInclude,
153 originalIncludeSubdomains: host.originalIncludeSubdomains };
154 }
155
156 function RedirectStopper() {};
157
158 RedirectStopper.prototype = {
159 // nsIChannelEventSink
160 asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
161 throw Cr.NS_ERROR_ENTITY_CHANGED;
162 },
163
164 getInterface: function(iid) {
165 return this.QueryInterface(iid);
166 },
167
168 QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink])
169 };
170
171 function getHSTSStatus(host, resultList) {
172 var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
173 .createInstance(Ci.nsIXMLHttpRequest);
174 var inResultList = false;
175 var uri = "https://" + host.name + "/";
176 req.open("GET", uri, true);
177 req.timeout = REQUEST_TIMEOUT;
178 req.channel.notificationCallbacks = new RedirectStopper();
179 req.onreadystatechange = function(event) {
180 if (!inResultList && req.readyState == 4) {
181 inResultList = true;
182 var header = req.getResponseHeader("strict-transport-security");
183 resultList.push(processStsHeader(host, header, req.status));
184 }
185 };
186
187 try {
188 req.send();
189 }
190 catch (e) {
191 dump("ERROR: exception making request to " + host.name + ": " + e + "\n");
192 }
193 }
194
195 function compareHSTSStatus(a, b) {
196 return (a.name > b.name ? 1 : (a.name < b.name ? -1 : 0));
197 }
198
199 function writeTo(string, fos) {
200 fos.write(string, string.length);
201 }
202
203 // Determines and returns a string representing a declaration of when this
204 // preload list should no longer be used.
205 // This is the current time plus MINIMUM_REQUIRED_MAX_AGE.
206 function getExpirationTimeString() {
207 var now = new Date();
208 var nowMillis = now.getTime();
209 // MINIMUM_REQUIRED_MAX_AGE is in seconds, so convert to milliseconds
210 var expirationMillis = nowMillis + (MINIMUM_REQUIRED_MAX_AGE * 1000);
211 var expirationMicros = expirationMillis * 1000;
212 return "const PRTime gPreloadListExpirationTime = INT64_C(" + expirationMicros + ");\n";
213 }
214
215 function errorToString(status) {
216 return (status.error == ERROR_MAX_AGE_TOO_LOW
217 ? status.error + status.maxAge
218 : status.error);
219 }
220
221 function writeEntry(status, outputStream) {
222 let incSubdomainsBool = (status.forceInclude && status.error != ERROR_NONE
223 ? status.originalIncludeSubdomains
224 : status.includeSubdomains);
225 let includeSubdomains = (incSubdomainsBool ? "true" : "false");
226 writeTo(" { \"" + status.name + "\", " + includeSubdomains + " },\n",
227 outputStream);
228 }
229
230 function output(sortedStatuses, currentList) {
231 try {
232 var file = FileUtils.getFile("CurWorkD", [OUTPUT]);
233 var errorFile = FileUtils.getFile("CurWorkD", [ERROR_OUTPUT]);
234 var fos = FileUtils.openSafeFileOutputStream(file);
235 var eos = FileUtils.openSafeFileOutputStream(errorFile);
236 writeTo(HEADER, fos);
237 writeTo(getExpirationTimeString(), fos);
238 writeTo(PREFIX, fos);
239 for (var status of sortedStatuses) {
240
241 // If we've encountered an error for this entry (other than the site not
242 // sending an HSTS header), be safe and don't remove it from the list
243 // (given that it was already on the list).
244 if (status.error != ERROR_NONE &&
245 status.error != ERROR_NO_HSTS_HEADER &&
246 status.error != ERROR_MAX_AGE_TOO_LOW &&
247 status.name in currentList) {
248 dump("INFO: error connecting to or processing " + status.name + " - using previous status on list\n");
249 writeTo(status.name + ": " + errorToString(status) + "\n", eos);
250 status.maxAge = MINIMUM_REQUIRED_MAX_AGE;
251 status.includeSubdomains = currentList[status.name];
252 }
253
254 if (status.maxAge >= MINIMUM_REQUIRED_MAX_AGE || status.forceInclude) {
255 writeEntry(status, fos);
256 dump("INFO: " + status.name + " ON the preload list\n");
257 if (status.forceInclude && status.error != ERROR_NONE) {
258 writeTo(status.name + ": " + errorToString(status) + " (error "
259 + "ignored - included regardless)\n", eos);
260 }
261 }
262 else {
263 dump("INFO: " + status.name + " NOT ON the preload list\n");
264 writeTo(status.name + ": " + errorToString(status) + "\n", eos);
265 }
266 }
267 writeTo(POSTFIX, fos);
268 FileUtils.closeSafeFileOutputStream(fos);
269 FileUtils.closeSafeFileOutputStream(eos);
270 }
271 catch (e) {
272 dump("ERROR: problem writing output to '" + OUTPUT + "': " + e + "\n");
273 }
274 }
275
276 function shouldRetry(response) {
277 return (response.error != ERROR_NO_HSTS_HEADER &&
278 response.error != ERROR_MAX_AGE_TOO_LOW &&
279 response.error != ERROR_NONE && response.retries > 0);
280 }
281
282 function getHSTSStatuses(inHosts, outStatuses) {
283 var expectedOutputLength = inHosts.length;
284 var tmpOutput = [];
285 for (var i = 0; i < MAX_CONCURRENT_REQUESTS && inHosts.length > 0; i++) {
286 var host = inHosts.shift();
287 dump("spinning off request to '" + host.name + "' (remaining retries: " +
288 host.retries + ")\n");
289 getHSTSStatus(host, tmpOutput);
290 }
291
292 while (outStatuses.length != expectedOutputLength) {
293 waitForAResponse(tmpOutput);
294 var response = tmpOutput.shift();
295 dump("request to '" + response.name + "' finished\n");
296 if (shouldRetry(response))
297 inHosts.push(response);
298 else
299 outStatuses.push(response);
300
301 if (inHosts.length > 0) {
302 var host = inHosts.shift();
303 dump("spinning off request to '" + host.name + "' (remaining retries: " +
304 host.retries + ")\n");
305 getHSTSStatus(host, tmpOutput);
306 }
307 }
308 }
309
310 // Since all events are processed on the main thread, and since event
311 // handlers are not preemptible, there shouldn't be any concurrency issues.
312 function waitForAResponse(outputList) {
313 // From <https://developer.mozilla.org/en/XPConnect/xpcshell/HOWTO>
314 var threadManager = Cc["@mozilla.org/thread-manager;1"]
315 .getService(Ci.nsIThreadManager);
316 var mainThread = threadManager.currentThread;
317 while (outputList.length == 0) {
318 mainThread.processNextEvent(true);
319 }
320 }
321
322 function readCurrentList(filename) {
323 var currentHosts = {};
324 var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
325 file.initWithPath(filename);
326 var fis = Cc["@mozilla.org/network/file-input-stream;1"]
327 .createInstance(Ci.nsILineInputStream);
328 fis.init(file, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
329 var line = {};
330 var entryRegex = / { "([^"]*)", (true|false) },/;
331 while (fis.readLine(line)) {
332 var match = entryRegex.exec(line.value);
333 if (match) {
334 currentHosts[match[1]] = (match[2] == "true");
335 }
336 }
337 return currentHosts;
338 }
339
340 function combineLists(newHosts, currentHosts) {
341 for (let currentHost in currentHosts) {
342 let found = false;
343 for (let newHost of newHosts) {
344 if (newHost.name == currentHost) {
345 found = true;
346 break;
347 }
348 }
349 if (!found) {
350 newHosts.push({ name: currentHost, retries: MAX_RETRIES });
351 }
352 }
353 }
354
355 // ****************************************************************************
356 // This is where the action happens:
357 if (arguments.length < 1) {
358 throw "Usage: getHSTSPreloadList.js <absolute path to current nsSTSPreloadList.inc>";
359 }
360 // get the current preload list
361 var currentHosts = readCurrentList(arguments[0]);
362 // disable the current preload list so it won't interfere with requests we make
363 Services.prefs.setBoolPref("network.stricttransportsecurity.preloadlist", false);
364 // download and parse the raw json file from the Chromium source
365 var rawdata = download();
366 // get just the hosts with mode: "force-https"
367 var hosts = getHosts(rawdata);
368 // add hosts in the current list to the new list (avoiding duplicates)
369 combineLists(hosts, currentHosts);
370 // get the HSTS status of each host
371 var hstsStatuses = [];
372 getHSTSStatuses(hosts, hstsStatuses);
373 // sort the hosts alphabetically
374 hstsStatuses.sort(compareHSTSStatus);
375 // write the results to a file (this is where we filter out hosts that we
376 // either couldn't connect to, didn't receive an HSTS header from, couldn't
377 // parse the header, or had a header with too short a max-age)
378 output(hstsStatuses, currentHosts);
379 // ****************************************************************************

mercurial