Thu, 22 Jan 2015 13:21:57 +0100
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 | // **************************************************************************** |