security/manager/tools/getHSTSPreloadList.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial