|
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 // **************************************************************************** |