--- a/dom/security/test/hsts/browser.ini
+++ b/dom/security/test/hsts/browser.ini
@@ -12,8 +12,9 @@ support-files =
[browser_hsts-priming_block_active.js]
[browser_hsts-priming_hsts_after_mixed.js]
[browser_hsts-priming_allow_display.js]
[browser_hsts-priming_block_display.js]
[browser_hsts-priming_block_active_css.js]
[browser_hsts-priming_block_active_with_redir_same.js]
[browser_hsts-priming_no-duplicates.js]
[browser_hsts-priming_cache-timeout.js]
+[browser_hsts-priming_timeout.js]
--- a/dom/security/test/hsts/browser_hsts-priming_no-duplicates.js
+++ b/dom/security/test/hsts/browser_hsts-priming_no-duplicates.js
@@ -14,17 +14,15 @@ add_task(function*() {
let which = "block_display";
SetupPrefTestEnvironment(which);
for (let server of Object.keys(test_servers)) {
yield execute_test(server, test_settings[which].mimetype);
}
- test_settings[which].priming = {};
-
// run the tests twice to validate the cache is being used
for (let server of Object.keys(test_servers)) {
yield execute_test(server, test_settings[which].mimetype);
}
SpecialPowers.popPrefEnv();
});
new file mode 100644
--- /dev/null
+++ b/dom/security/test/hsts/browser_hsts-priming_timeout.js
@@ -0,0 +1,24 @@
+/*
+ * Description of the test:
+ * Only one request should be sent per host, even if we run the test more
+ * than once.
+ */
+'use strict';
+
+//jscs:disable
+add_task(function*() {
+ //jscs:enable
+ Observer.add_observers(Services);
+ registerCleanupFunction(do_cleanup);
+
+ let which = "timeout";
+
+ SetupPrefTestEnvironment(which, [["security.mixed_content.hsts_priming_request_timeout",
+ 1000]]);
+
+ for (let server of Object.keys(test_servers)) {
+ yield execute_test(server, test_settings[which].mimetype);
+ }
+
+ SpecialPowers.popPrefEnv();
+});
--- a/dom/security/test/hsts/file_priming-top.html
+++ b/dom/security/test/hsts/file_priming-top.html
@@ -32,24 +32,28 @@ var args = parse_query_string();
var subresources = {
css: { mimetype: 'text/css', file: 'file_stylesheet.css' },
img: { mimetype: 'image/png', file: 'file_1x1.png' },
script: { mimetype: 'text/javascript', file: 'file_priming.js' },
};
function handler(ev) {
console.log("HSTS_PRIMING: Blocked "+args.id);
+ let elem = document.getElementById(args.id);
+ elem.parentElement.removeChild(elem);
}
function loadCss(src) {
let head = document.getElementsByTagName("head")[0];
let link = document.createElement("link");
link.setAttribute("rel", "stylesheet");
+ link.setAttribute("id", args.id);
link.setAttribute("type", subresources[args.type].mimetype);
link.setAttribute("href", src);
+ link.onerror = handler;
head.appendChild(link);
}
function loadResource(src) {
let content = document.getElementById("content");
let testElem = document.createElement(args.type);
testElem.setAttribute("id", args.id);
testElem.setAttribute("charset", "UTF-8");
@@ -62,16 +66,17 @@ function loadTest() {
let subresource = subresources[args.type];
let src = "http://"
+ args.host
+ "/browser/dom/security/test/hsts/file_testserver.sjs"
+ "?file=" +escape("browser/dom/security/test/hsts/" + subresource.file)
+ "&primer=" + escape(args.id)
+ "&mimetype=" + escape(subresource.mimetype)
+ + "&timeout=" + escape(args.timeout)
;
if (args.type == 'css') {
loadCss(src);
return;
}
loadResource(src);
}
--- a/dom/security/test/hsts/file_stylesheet.css
+++ b/dom/security/test/hsts/file_stylesheet.css
@@ -0,0 +1,1 @@
+body {}
--- a/dom/security/test/hsts/file_testserver.sjs
+++ b/dom/security/test/hsts/file_testserver.sjs
@@ -22,45 +22,63 @@ function loadFromFile(path) {
var test = NetUtil.readInputStreamToString(testFileStream, testFileStream.available());
return test;
}
function handleRequest(request, response)
{
const query = new URLSearchParams(request.queryString);
- redir = query.get('redir');
- if (redir == 'same') {
- query.delete("redir");
- response.setStatus(302);
- let newURI = request.uri;
- newURI.queryString = query.serialize();
- response.setHeader("Location", newURI.spec)
- }
+ var timeout = parseInt(query.get('timeout'));
+ response.processAsync();
+
+ timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+ timer.initWithCallback(function()
+ {
+ if (!response) {
+ return;
+ }
- // avoid confusing cache behaviors
- response.setHeader("Cache-Control", "no-cache", false);
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ redir = query.get('redir');
+ if (redir == 'same') {
+ query.delete("redir");
+ response.setStatus(302);
+ let newURI = request.uri;
+ newURI.queryString = query.serialize();
+ response.setHeader("Location", newURI.spec)
+ response.write('xyzzy');
+ response.finish();
+ return;
+ }
- // if we have a priming header, check for required behavior
- // and set header appropriately
- if (request.hasHeader('Upgrade-Insecure-Requests')) {
- var expected = query.get('primer');
- if (expected == 'prime-hsts') {
- // set it for 5 minutes
- response.setHeader("Strict-Transport-Security", "max-age="+(60*5), false);
- } else if (expected == 'reject-upgrade') {
- response.setHeader("Strict-Transport-Security", "max-age=0", false);
- }
- response.write('');
- return;
- }
+ // if we have a priming header, check for required behavior
+ // and set header appropriately
+ if (request.hasHeader('Upgrade-Insecure-Requests')) {
+ var expected = query.get('primer');
+ if (expected == 'prime-hsts') {
+ // set it for 5 minutes
+ response.setHeader("Strict-Transport-Security", "max-age="+(60*5), false);
+ } else if (expected == 'reject-upgrade') {
+ response.setHeader("Strict-Transport-Security", "max-age=0", false);
+ }
+ response.write('xyzzy');
+ response.finish();
+ return;
+ }
- var file = query.get('file');
- if (file) {
- var mimetype = unescape(query.get('mimetype'));
- response.setHeader("Content-Type", mimetype, false);
- response.write(loadFromFile(unescape(file)));
- return;
- }
+ var file = query.get('file');
+ if (file) {
+ var mimetype = unescape(query.get('mimetype'));
+ response.setHeader("Content-Type", mimetype, false);
+ let contents = loadFromFile(unescape(file));
+ response.write(contents);
+ response.finish();
+ return;
+ }
- response.setHeader("Content-Type", "application/json", false);
- response.write('{}');
+ response.setHeader("Content-Type", "application/json", false);
+ response.write('{}');
+ response.finish();
+ }, timeout, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
}
--- a/dom/security/test/hsts/head.js
+++ b/dom/security/test/hsts/head.js
@@ -42,108 +42,164 @@ var test_servers = {
var test_settings = {
// mixed active content is allowed, priming will upgrade
allow_active: {
block_active: false,
block_display: false,
use_hsts: true,
send_hsts_priming: true,
type: 'script',
+ timeout: 0,
result: {
'no-ssl': 'insecure',
'reject-upgrade': 'insecure',
'prime-hsts': 'secure',
},
},
// mixed active content is blocked, priming will upgrade
block_active: {
block_active: true,
block_display: false,
use_hsts: true,
send_hsts_priming: true,
type: 'script',
+ timeout: 0,
result: {
'no-ssl': 'blocked',
'reject-upgrade': 'blocked',
'prime-hsts': 'secure',
},
},
// keep the original order of mixed-content and HSTS, but send
// priming requests
hsts_after_mixed: {
block_active: true,
block_display: false,
use_hsts: false,
send_hsts_priming: true,
type: 'script',
+ timeout: 0,
result: {
'no-ssl': 'blocked',
'reject-upgrade': 'blocked',
'prime-hsts': 'blocked',
},
},
// mixed display content is allowed, priming will upgrade
allow_display: {
block_active: true,
block_display: false,
use_hsts: true,
send_hsts_priming: true,
type: 'img',
+ timeout: 0,
result: {
'no-ssl': 'insecure',
'reject-upgrade': 'insecure',
'prime-hsts': 'secure',
},
},
// mixed display content is blocked, priming will upgrade
block_display: {
block_active: true,
block_display: true,
use_hsts: true,
send_hsts_priming: true,
type: 'img',
+ timeout: 0,
result: {
'no-ssl': 'blocked',
'reject-upgrade': 'blocked',
'prime-hsts': 'secure',
},
},
// mixed active content is blocked, priming will upgrade (css)
block_active_css: {
block_active: true,
- block_display: false,
+ block_display: true,
use_hsts: true,
send_hsts_priming: true,
type: 'css',
+ timeout: 0,
result: {
'no-ssl': 'blocked',
'reject-upgrade': 'blocked',
'prime-hsts': 'secure',
},
},
// mixed active content is blocked, priming will upgrade
// redirect to the same host
block_active_with_redir_same: {
block_active: true,
block_display: false,
use_hsts: true,
send_hsts_priming: true,
type: 'script',
redir: 'same',
+ timeout: 0,
result: {
'no-ssl': 'blocked',
'reject-upgrade': 'blocked',
'prime-hsts': 'secure',
},
},
+ // mixed active content is blocked, priming will upgrade
+ // redirect to the same host
+ timeout: {
+ block_active: true,
+ block_display: true,
+ use_hsts: true,
+ send_hsts_priming: true,
+ type: 'script',
+ timeout: 100000,
+ result: {
+ 'no-ssl': 'blocked',
+ 'reject-upgrade': 'blocked',
+ 'prime-hsts': 'blocked',
+ },
+ },
}
// track which test we are on
var which_test = "";
-const Observer = {
+/**
+ * A stream listener that just forwards all calls
+ */
+var StreamListener = function(subject) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ let traceable = subject.QueryInterface(Ci.nsITraceableChannel);
+
+ this.uri = channel.URI.asciiSpec;
+ this.listener = traceable.setNewListener(this);
+ return this;
+};
+
+// Next three methods are part of `nsIStreamListener` interface and are
+// invoked by `nsIInputStreamPump.asyncRead`.
+StreamListener.prototype.onDataAvailable = function(request, context, input, offset, count) {
+ let listener = this.listener;
+ listener.onDataAvailable(request, context, input, offset, count);
+};
+
+// Next two methods implement `nsIRequestObserver` interface and are invoked
+// by `nsIInputStreamPump.asyncRead`.
+StreamListener.prototype.onStartRequest = function(request, context) {
+ let listener = this.listener;
+ listener.onStartRequest(request, context);
+};
+
+// Called to signify the end of an asynchronous request. We only care to
+// discover errors.
+StreamListener.prototype.onStopRequest = function(request, context, status) {
+ let listener = this.listener;
+ listener.onStopRequest(request, context, status);
+};
+
+var Observer = {
+ listeners: {},
observe: function (subject, topic, data) {
switch (topic) {
case 'console-api-log-event':
return Observer.console_api_log_event(subject, topic, data);
case 'http-on-examine-response':
return Observer.http_on_examine_response(subject, topic, data);
case 'http-on-modify-request':
return Observer.http_on_modify_request(subject, topic, data);
@@ -186,34 +242,45 @@ const Observer = {
if (re.test(uri)) {
return test_servers[item];
}
}
return null;
},
http_on_modify_request: function (subject, topic, data) {
let channel = subject.QueryInterface(Ci.nsIHttpChannel);
- if (channel.requestMethod != 'HEAD') {
- return;
- }
+ let uri = channel.URI.asciiSpec;
let curTest = this.get_current_test(channel.URI.asciiSpec);
if (!curTest) {
return;
}
+
+ if (!(uri in this.listeners)) {
+ // Add an nsIStreamListener to ensure that the listener is not NULL
+ this.listeners[uri] = new StreamListener(subject);
+ }
+ if (channel.requestMethod != 'HEAD') {
+ return;
+ }
+ if (typeof ok === 'undefined') {
+ // we are in the wrong thread and ok and is not available
+ return;
+ }
ok(!(curTest.id in test_settings[which_test].priming), "Already saw a priming request for " + curTest.id);
test_settings[which_test].priming[curTest.id] = true;
},
// When we see a response come back, peek at the response and test it is secure
// or insecure as needed. Addtionally, watch the response for priming requests.
http_on_examine_response: function (subject, topic, data) {
let channel = subject.QueryInterface(Ci.nsIHttpChannel);
let curTest = this.get_current_test(channel.URI.asciiSpec);
+ let uri = channel.URI.asciiSpec;
if (!curTest) {
return;
}
let result = (channel.URI.asciiSpec.startsWith('https:')) ? "secure" : "insecure";
// This is a priming request, go ahead and validate we were supposed to see
@@ -222,16 +289,19 @@ const Observer = {
is(true, curTest.response, "HSTS priming response found " + curTest.id);
return;
}
// This is the response to our query, make sure it matches
is(result, test_settings[which_test].result[curTest.id],
"HSTS priming result " + which_test + ":" + curTest.id);
test_settings[which_test].finished[curTest.id] = result;
+ if (this.listeners[uri]) {
+ this.listeners[uri] = undefined;
+ }
},
};
// opens `uri' in a new tab and focuses it.
// returns the newly opened tab
function openTab(uri) {
let tab = gBrowser.addTab(uri);
@@ -267,41 +337,43 @@ function SetupPrefTestEnvironment(which,
var prefs = [["security.mixed_content.block_active_content",
settings.block_active],
["security.mixed_content.block_display_content",
settings.block_display],
["security.mixed_content.use_hsts",
settings.use_hsts],
["security.mixed_content.send_hsts_priming",
- settings.send_hsts_priming]];
+ settings.send_hsts_priming],
+ ];
if (additional_prefs) {
for (let idx in additional_prefs) {
prefs.push(additional_prefs[idx]);
}
}
- console.log("prefs=%s", prefs);
-
SpecialPowers.pushPrefEnv({'set': prefs});
}
// make the top-level test uri
-function build_test_uri(base_uri, host, test_id, type) {
+function build_test_uri(base_uri, host, test_id, type, timeout) {
return base_uri +
"?host=" + escape(host) +
"&id=" + escape(test_id) +
- "&type=" + escape(type);
+ "&type=" + escape(type) +
+ "&timeout=" + escape(timeout)
+ ;
}
// open a new tab, load the test, and wait for it to finish
function execute_test(test, mimetype) {
var src = build_test_uri(TOP_URI, test_servers[test].host,
- test, test_settings[which_test].type);
+ test, test_settings[which_test].type,
+ test_settings[which_test].timeout);
let tab = openTab(src);
test_servers[test]['tab'] = tab;
let browser = gBrowser.getBrowserForTab(tab);
yield BrowserTestUtils.browserLoaded(browser);
yield BrowserTestUtils.removeTab(tab);
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5541,18 +5541,21 @@ pref("security.mixed_content.send_hsts_p
// Don't change the order of evaluation of mixed-content and HSTS upgrades in
// order to be most compatible with current standards
pref("security.mixed_content.use_hsts", false);
#else
// Change the order of evaluation so HSTS upgrades happen before
// mixed-content blocking
pref("security.mixed_content.use_hsts", true);
#endif
-// Approximately 1 week default cache for HSTS priming failures
+// Approximately 1 week default cache for HSTS priming failures, in seconds
pref ("security.mixed_content.hsts_priming_cache_timeout", 10080);
+// Force the channel to timeout in 3 seconds if we have not received
+// expects a time in milliseconds
+pref ("security.mixed_content.hsts_priming_request_timeout", 3000);
// Disable Storage api in release builds.
#ifdef NIGHTLY_BUILD
pref("dom.storageManager.enabled", true);
#else
pref("dom.storageManager.enabled", false);
#endif
--- a/netwerk/protocol/http/HSTSPrimerListener.cpp
+++ b/netwerk/protocol/http/HSTSPrimerListener.cpp
@@ -10,55 +10,88 @@
#include "nsIHstsPrimingCallback.h"
#include "nsIPrincipal.h"
#include "nsSecurityHeaderParser.h"
#include "nsISiteSecurityService.h"
#include "nsISocketProvider.h"
#include "nsISSLStatus.h"
#include "nsISSLStatusProvider.h"
#include "nsStreamUtils.h"
+#include "nsStreamListenerWrapper.h"
#include "nsHttpChannel.h"
#include "LoadInfo.h"
+#include "mozilla/Unused.h"
namespace mozilla {
namespace net {
using namespace mozilla;
NS_IMPL_ISUPPORTS(HSTSPrimingListener, nsIStreamListener,
- nsIRequestObserver, nsIInterfaceRequestor)
+ nsIRequestObserver, nsIInterfaceRequestor,
+ nsITimerCallback)
+
+// default to 3000ms, same as the preference
+uint32_t HSTSPrimingListener::sHSTSPrimingTimeout = 3000;
+
+
+HSTSPrimingListener::HSTSPrimingListener(nsIHstsPrimingCallback* aCallback)
+ : mCallback(aCallback)
+{
+ static nsresult rv =
+ Preferences::AddUintVarCache(&sHSTSPrimingTimeout,
+ "security.mixed_content.hsts_priming_request_timeout");
+ Unused << rv;
+}
NS_IMETHODIMP
HSTSPrimingListener::GetInterface(const nsIID & aIID, void **aResult)
{
return QueryInterface(aIID, aResult);
}
-NS_IMETHODIMP
-HSTSPrimingListener::OnStartRequest(nsIRequest *aRequest,
- nsISupports *aContext)
+void
+HSTSPrimingListener::ReportTiming(nsresult aResult)
{
- nsresult primingResult = CheckHSTSPrimingRequestStatus(aRequest);
- nsCOMPtr<nsIHstsPrimingCallback> callback(mCallback);
- mCallback = nullptr;
-
nsCOMPtr<nsITimedChannel> timingChannel =
- do_QueryInterface(callback);
+ do_QueryInterface(mCallback);
if (timingChannel) {
TimeStamp channelCreationTime;
nsresult rv = timingChannel->GetChannelCreation(&channelCreationTime);
if (NS_SUCCEEDED(rv) && !channelCreationTime.IsNull()) {
PRUint32 interval =
(PRUint32) (TimeStamp::Now() - channelCreationTime).ToMilliseconds();
Telemetry::Accumulate(Telemetry::HSTS_PRIMING_REQUEST_DURATION,
- (NS_SUCCEEDED(primingResult)) ? NS_LITERAL_CSTRING("success")
- : NS_LITERAL_CSTRING("failure"),
+ (NS_SUCCEEDED(aResult)) ? NS_LITERAL_CSTRING("success")
+ : NS_LITERAL_CSTRING("failure"),
interval);
}
}
+}
+
+NS_IMETHODIMP
+HSTSPrimingListener::OnStartRequest(nsIRequest *aRequest,
+ nsISupports *aContext)
+{
+ nsCOMPtr<nsIHstsPrimingCallback> callback;
+ callback.swap(mCallback);
+
+ if (mHSTSPrimingTimer) {
+ Unused << mHSTSPrimingTimer->Cancel();
+ mHSTSPrimingTimer = nullptr;
+ }
+
+ // if callback is null, we have already canceled this request and reported
+ // the failure
+ if (!callback) {
+ return NS_OK;
+ }
+
+ nsresult primingResult = CheckHSTSPrimingRequestStatus(aRequest);
+ ReportTiming(primingResult);
if (NS_FAILED(primingResult)) {
LOG(("HSTS Priming Failed (request was not approved)"));
return callback->OnHSTSPrimingFailed(primingResult, false);
}
LOG(("HSTS Priming Succeeded (request was approved)"));
return callback->OnHSTSPrimingSucceeded(false);
@@ -134,22 +167,50 @@ HSTSPrimingListener::OnDataAvailable(nsI
nsIInputStream *inStr,
uint64_t sourceOffset,
uint32_t count)
{
uint32_t totalRead;
return inStr->ReadSegments(NS_DiscardSegment, nullptr, count, &totalRead);
}
+/** nsITimerCallback **/
+NS_IMETHODIMP
+HSTSPrimingListener::Notify(nsITimer* timer)
+{
+ nsresult rv;
+ nsCOMPtr<nsIHstsPrimingCallback> callback;
+ callback.swap(mCallback);
+ if (!callback) {
+ // we already processed this channel
+ return NS_OK;
+ }
+
+ ReportTiming(NS_ERROR_HSTS_PRIMING_TIMEOUT);
+
+ if (mPrimingChannel) {
+ rv = mPrimingChannel->Cancel(NS_ERROR_HSTS_PRIMING_TIMEOUT);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("HSTS Priming timed out, and we got an error canceling the priming channel.");
+ }
+ }
+
+ rv = callback->OnHSTSPrimingFailed(NS_ERROR_HSTS_PRIMING_TIMEOUT, false);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("HSTS Priming timed out, and we got an error reporting the failure.");
+ }
+
+ return NS_OK; // unused
+}
+
// static
nsresult
HSTSPrimingListener::StartHSTSPriming(nsIChannel* aRequestChannel,
nsIHstsPrimingCallback* aCallback)
{
-
nsCOMPtr<nsIURI> finalChannelURI;
nsresult rv = NS_GetFinalChannelURI(aRequestChannel, getter_AddRefs(finalChannelURI));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIURI> uri;
rv = NS_GetSecureUpgradedURI(finalChannelURI, getter_AddRefs(uri));
NS_ENSURE_SUCCESS(rv,rv);
@@ -225,16 +286,18 @@ HSTSPrimingListener::StartHSTSPriming(ns
NS_ENSURE_SUCCESS(rv, rv);
// Set method and headers
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(primingChannel);
if (!httpChannel) {
NS_ERROR("HSTSPrimingListener: Failed to QI to nsIHttpChannel!");
return NS_ERROR_FAILURE;
}
+ nsCOMPtr<nsIHttpChannelInternal> internal = do_QueryInterface(primingChannel);
+ NS_ENSURE_STATE(internal);
// Currently using HEAD per the draft, but under discussion to change to GET
// with credentials so if the upgrade is approved the result is already cached.
rv = httpChannel->SetRequestMethod(NS_LITERAL_CSTRING("HEAD"));
NS_ENSURE_SUCCESS(rv, rv);
rv = httpChannel->
SetRequestHeader(NS_LITERAL_CSTRING("Upgrade-Insecure-Requests"),
@@ -244,30 +307,51 @@ HSTSPrimingListener::StartHSTSPriming(ns
// attempt to set the class of service flags on the new channel
nsCOMPtr<nsIClassOfService> requestClass = do_QueryInterface(aRequestChannel);
if (!requestClass) {
NS_ERROR("HSTSPrimingListener: aRequestChannel is not an nsIClassOfService");
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIClassOfService> primingClass = do_QueryInterface(httpChannel);
if (!primingClass) {
- NS_ERROR("HSTSPrimingListener: aRequestChannel is not an nsIClassOfService");
+ NS_ERROR("HSTSPrimingListener: httpChannel is not an nsIClassOfService");
return NS_ERROR_FAILURE;
}
uint32_t classFlags = 0;
rv = requestClass ->GetClassFlags(&classFlags);
NS_ENSURE_SUCCESS(rv, rv);
rv = primingClass->SetClassFlags(classFlags);
NS_ENSURE_SUCCESS(rv, rv);
- // Set up listener which will start the original channel
- nsCOMPtr<nsIStreamListener> primingListener(new HSTSPrimingListener(aCallback));
+ // The priming channel should have highest priority so that it completes as
+ // quickly as possible, allowing the load to proceed.
+ nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(primingChannel);
+ if (p) {
+ uint32_t priority = nsISupportsPriority::PRIORITY_HIGHEST;
+
+ p->SetPriority(priority);
+ }
+ // Set up listener which will start the original channel
+ HSTSPrimingListener* listener = new HSTSPrimingListener(aCallback);
// Start priming
- rv = primingChannel->AsyncOpen2(primingListener);
+ rv = primingChannel->AsyncOpen2(listener);
NS_ENSURE_SUCCESS(rv, rv);
+ listener->mPrimingChannel.swap(primingChannel);
+
+ nsCOMPtr<nsITimer> timer = do_CreateInstance(NS_TIMER_CONTRACTID);
+ NS_ENSURE_STATE(timer);
+
+ rv = timer->InitWithCallback(listener,
+ sHSTSPrimingTimeout,
+ nsITimer::TYPE_ONE_SHOT);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("HSTS Priming failed to initialize channel cancellation timer");
+ }
+
+ listener->mHSTSPrimingTimer.swap(timer);
return NS_OK;
}
} // namespace net
} // namespace mozilla
--- a/netwerk/protocol/http/HSTSPrimerListener.h
+++ b/netwerk/protocol/http/HSTSPrimerListener.h
@@ -44,36 +44,41 @@ enum HSTSPrimingResult {
// of mixed-content and hsts, and mixed-content blocks the load
eHSTS_PRIMING_SUCCEEDED_BLOCK = 5,
// When priming succeeds, but preferences require preservation of the order
// of mixed-content and hsts, and mixed-content allows the load over http
eHSTS_PRIMING_SUCCEEDED_HTTP = 6,
// HSTS priming failed, and the load is blocked by mixed-content
eHSTS_PRIMING_FAILED_BLOCK = 7,
// HSTS priming failed, and the load is allowed by mixed-content
- eHSTS_PRIMING_FAILED_ACCEPT = 8
+ eHSTS_PRIMING_FAILED_ACCEPT = 8,
+ // The HSTS Priming request timed out, and the load is blocked by
+ // mixed-content
+ eHSTS_PRIMING_TIMEOUT_BLOCK = 9,
+ // The HSTS Priming request timed out, and the load is allowed by
+ // mixed-content
+ eHSTS_PRIMING_TIMEOUT_ACCEPT = 10
};
//////////////////////////////////////////////////////////////////////////
// Class used as streamlistener and notification callback when
// doing the HEAD request for an HSTS Priming check. Needs to be an
// nsIStreamListener in order to receive events from AsyncOpen2
class HSTSPrimingListener final : public nsIStreamListener,
- public nsIInterfaceRequestor
+ public nsIInterfaceRequestor,
+ public nsITimerCallback
{
public:
- explicit HSTSPrimingListener(nsIHstsPrimingCallback* aCallback)
- : mCallback(aCallback)
- {
- }
+ explicit HSTSPrimingListener(nsIHstsPrimingCallback* aCallback);
NS_DECL_ISUPPORTS
NS_DECL_NSISTREAMLISTENER
NS_DECL_NSIREQUESTOBSERVER
NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSITIMERCALLBACK
private:
~HSTSPrimingListener() {}
// Only nsHttpChannel can invoke HSTS priming
friend class mozilla::net::nsHttpChannel;
/**
@@ -91,18 +96,38 @@ private:
nsIHstsPrimingCallback* aCallback);
/**
* Given a request, return NS_OK if it has resulted in a cached HSTS update.
* We don't need to check for the header as that has already been done for us.
*/
nsresult CheckHSTSPrimingRequestStatus(nsIRequest* aRequest);
+ // send telemetry about how long HSTS priming requests take
+ void ReportTiming(nsresult aResult);
+
/**
* the nsIHttpChannel to notify with the result of HSTS priming.
*/
nsCOMPtr<nsIHstsPrimingCallback> mCallback;
+
+ /**
+ * Keep a handle to the priming channel so we can cancel it on timeout
+ */
+ nsCOMPtr<nsIChannel> mPrimingChannel;
+
+ /**
+ * Keep a handle to the timer around so it can be canceled if we don't time
+ * out.
+ */
+ nsCOMPtr<nsITimer> mHSTSPrimingTimer;
+
+ /**
+ * How long (in ms) before an HSTS Priming channel times out.
+ * Preference: security.mixed_content.hsts_priming_request_timeout
+ */
+ static uint32_t sHSTSPrimingTimeout;
};
}} // mozilla::net
#endif // HSTSPrimingListener_h__
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -8089,29 +8089,34 @@ nsHttpChannel::OnHSTSPrimingSucceeded(bo
*/
nsresult
nsHttpChannel::OnHSTSPrimingFailed(nsresult aError, bool aCached)
{
bool wouldBlock = mLoadInfo->GetMixedContentWouldBlock();
LOG(("HSTS Priming Failed [this=%p], %s the load", this,
(wouldBlock) ? "blocking" : "allowing"));
- if (aCached) {
+ if (aError == NS_ERROR_HSTS_PRIMING_TIMEOUT) {
+ // A priming request was sent, but timed out
+ Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING_RESULT,
+ (wouldBlock) ? HSTSPrimingResult::eHSTS_PRIMING_TIMEOUT_BLOCK :
+ HSTSPrimingResult::eHSTS_PRIMING_TIMEOUT_ACCEPT);
+ } else if (aCached) {
// Between the time we marked for priming and started the priming request,
// the host was found to not allow the upgrade, probably from another
// priming request.
Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING_RESULT,
(wouldBlock) ? HSTSPrimingResult::eHSTS_PRIMING_CACHED_BLOCK :
- HSTSPrimingResult::eHSTS_PRIMING_CACHED_NO_UPGRADE);
+ HSTSPrimingResult::eHSTS_PRIMING_CACHED_NO_UPGRADE);
} else {
// A priming request was sent, and no HSTS header was found that allows
// the upgrade.
Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING_RESULT,
(wouldBlock) ? HSTSPrimingResult::eHSTS_PRIMING_FAILED_BLOCK :
- HSTSPrimingResult::eHSTS_PRIMING_FAILED_ACCEPT);
+ HSTSPrimingResult::eHSTS_PRIMING_FAILED_ACCEPT);
}
// Don't visit again for at least
// security.mixed_content.hsts_priming_cache_timeout seconds.
nsISiteSecurityService* sss = gHttpHandler->GetSSService();
NS_ENSURE_TRUE(sss, NS_ERROR_OUT_OF_MEMORY);
nsresult rv = sss->CacheNegativeHSTSResult(mURI,
nsMixedContentBlocker::sHSTSPrimingCacheTimeout);
--- a/netwerk/protocol/http/nsIHstsPrimingCallback.idl
+++ b/netwerk/protocol/http/nsIHstsPrimingCallback.idl
@@ -28,16 +28,17 @@ interface nsIHstsPrimingCallback : nsISu
*
* May be invoked synchronously if HSTS priming has already been performed
* for the host.
*
* @param aCached whether the result was already in the HSTS cache
*/
[noscript, nostdcall]
void onHSTSPrimingSucceeded(in bool aCached);
+
/**
* HSTS priming has seen no STS header, the request itself has failed,
* or some other failure which does not constitute a positive signal that the
* site can be upgraded safely to HTTPS. The request may still be allowed
* based on the user's preferences.
*
* May be invoked synchronously if HSTS priming has already been performed
* for the host.
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -8226,17 +8226,17 @@
"description": "How often would blocked mixed content be allowed if HSTS upgrades were allowed, including how often would we send an HSTS priming request? 0=display/no-HSTS, 1=display/HSTS, 2=active/no-HSTS, 3=active/HSTS, 4=display/no-HSTS-priming, 5=display/do-HSTS-priming, 6=active/no-HSTS-priming, 7=active/do-HSTS-priming"
},
"MIXED_CONTENT_HSTS_PRIMING_RESULT": {
"alert_emails": ["seceng@mozilla.org"],
"bug_numbers": [1246540],
"expires_in_version": "60",
"kind": "enumerated",
"n_values": 16,
- "description": "How often do we get back an HSTS priming result which upgrades the connection to HTTPS? 0=cached (no upgrade), 1=cached (do upgrade), 2=cached (blocked), 3=already upgraded, 4=priming succeeded, 5=priming succeeded (block due to pref), 6=priming succeeded (no upgrade due to pref), 7=priming failed (block), 8=priming failed (accept)"
+ "description": "How often do we get back an HSTS priming result which upgrades the connection to HTTPS? 0=cached (no upgrade), 1=cached (do upgrade), 2=cached (blocked), 3=already upgraded, 4=priming succeeded, 5=priming succeeded (block due to pref), 6=priming succeeded (no upgrade due to pref), 7=priming failed (block), 8=priming failed (accept), 9=timeout (block), 10=timeout (accept)"
},
"HSTS_PRIMING_REQUEST_DURATION": {
"alert_emails": ["seceng-telemetry@mozilla.org"],
"bug_numbers": [1311893],
"expires_in_version": "58",
"kind": "exponential",
"low": 100,
"high": 30000,
--- a/xpcom/base/ErrorList.h
+++ b/xpcom/base/ErrorList.h
@@ -333,16 +333,20 @@
ERROR(NS_NET_STATUS_CONNECTED_TO, FAILURE(4)),
ERROR(NS_NET_STATUS_SENDING_TO, FAILURE(5)),
ERROR(NS_NET_STATUS_WAITING_FOR, FAILURE(10)),
ERROR(NS_NET_STATUS_RECEIVING_FROM, FAILURE(6)),
/* nsIInterceptedChannel */
/* Generic error for non-specific failures during service worker interception */
ERROR(NS_ERROR_INTERCEPTION_FAILED, FAILURE(100)),
+
+ /* nsIHstsPrimingListener */
+ /* Error code for HSTS priming timeout to distinguish from blocking */
+ ERROR(NS_ERROR_HSTS_PRIMING_TIMEOUT, FAILURE(110)),
#undef MODULE
/* ======================================================================= */
/* 7: NS_ERROR_MODULE_PLUGINS */
/* ======================================================================= */
#define MODULE NS_ERROR_MODULE_PLUGINS
ERROR(NS_ERROR_PLUGINS_PLUGINSNOTCHANGED, FAILURE(1000)),