Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/Components.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/glean/GleanMetrics.h"
#include "mozilla/NullPrincipal.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/net/DNS.h"
#include "nsContentUtils.h"
#include "nsHTTPSOnlyUtils.h"
#include "nsIConsoleService.h"
#include "nsIHttpChannel.h"
#include "nsIHttpChannelInternal.h"
#include "nsIHttpsOnlyModePermission.h"
#include "nsILoadInfo.h"
#include "nsIPermissionManager.h"
#include "nsIPrincipal.h"
#include "nsIRedirectHistoryEntry.h"
#include "nsIScriptError.h"
#include "nsIURIMutator.h"
#include "nsNetUtil.h"
#include "prnetdb.h"
/* static */
bool nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(bool aFromPrivateWindow) {
// if the general pref is set to true, then we always return
if (mozilla::StaticPrefs::dom_security_https_only_mode()) {
return true;
}
// otherwise we check if executing in private browsing mode and return true
// if the PBM pref for HTTPS-Only is set.
if (aFromPrivateWindow &&
mozilla::StaticPrefs::dom_security_https_only_mode_pbm()) {
return true;
}
return false;
}
/* static */
bool nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(bool aFromPrivateWindow) {
// HTTPS-Only takes priority over HTTPS-First
if (IsHttpsOnlyModeEnabled(aFromPrivateWindow)) {
return false;
}
// if the general pref is set to true, then we always return
if (mozilla::StaticPrefs::dom_security_https_first()) {
return true;
}
// otherwise we check if executing in private browsing mode and return true
// if the PBM pref for HTTPS-First is set.
if (aFromPrivateWindow &&
mozilla::StaticPrefs::dom_security_https_first_pbm()) {
return true;
}
return false;
}
/* static */
void nsHTTPSOnlyUtils::PotentiallyFireHttpRequestToShortenTimout(
mozilla::net::DocumentLoadListener* aDocumentLoadListener) {
// only send http background request to counter timeouts if the
// pref allows us to do that.
if (!mozilla::StaticPrefs::
dom_security_https_only_mode_send_http_background_request()) {
return;
}
nsCOMPtr<nsIChannel> channel = aDocumentLoadListener->GetChannel();
if (!channel) {
return;
}
nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
// if neither HTTPS-Only nor HTTPS-First mode is enabled, then there is
// nothing to do here.
if ((!IsHttpsOnlyModeEnabled(isPrivateWin) &&
!IsHttpsFirstModeEnabled(isPrivateWin)) &&
!(loadInfo->GetWasSchemelessInput() &&
mozilla::StaticPrefs::dom_security_https_first_schemeless())) {
return;
}
// if we are not dealing with a top-level load, then there is nothing to do
// here.
if (loadInfo->GetExternalContentPolicyType() !=
ExtContentPolicy::TYPE_DOCUMENT) {
return;
}
// if the load is exempt, then there is nothing to do here.
uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
if (httpsOnlyStatus & nsILoadInfo::nsILoadInfo::HTTPS_ONLY_EXEMPT) {
return;
}
// if it's not an http channel, then there is nothing to do here.
nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel));
if (!httpChannel) {
return;
}
// if it's not a GET method, then there is nothing to do here either.
nsAutoCString method;
mozilla::Unused << httpChannel->GetRequestMethod(method);
if (!method.EqualsLiteral("GET")) {
return;
}
// if it's already an https channel, then there is nothing to do here.
nsCOMPtr<nsIURI> channelURI;
channel->GetURI(getter_AddRefs(channelURI));
if (!channelURI->SchemeIs("http")) {
return;
}
// HTTPS-First only applies to standard ports but HTTPS-Only brute forces
// all http connections to be https and overrules HTTPS-First. In case
// HTTPS-First is enabled, but HTTPS-Only is not enabled, we might return
// early if attempting to send a background request to a non standard port.
if ((IsHttpsFirstModeEnabled(isPrivateWin) ||
(loadInfo->GetWasSchemelessInput() &&
mozilla::StaticPrefs::dom_security_https_first_schemeless()))) {
int32_t port = 0;
nsresult rv = channelURI->GetPort(&port);
int defaultPortforScheme = NS_GetDefaultPort("http");
if (NS_SUCCEEDED(rv) && port != defaultPortforScheme && port != -1) {
return;
}
}
// Check for general exceptions
if (OnionException(channelURI) || LoopbackOrLocalException(channelURI)) {
return;
}
RefPtr<nsIRunnable> task =
new TestHTTPAnswerRunnable(channelURI, aDocumentLoadListener);
NS_DispatchToMainThread(task.forget());
}
/* static */
bool nsHTTPSOnlyUtils::ShouldUpgradeRequest(nsIURI* aURI,
nsILoadInfo* aLoadInfo) {
// 1. Check if the HTTPS-Only Mode is even enabled, before we do anything else
bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
if (!IsHttpsOnlyModeEnabled(isPrivateWin)) {
return false;
}
// 2. Check for general exceptions
if (OnionException(aURI) || LoopbackOrLocalException(aURI)) {
return false;
}
// 3. Check if NoUpgrade-flag is set in LoadInfo
uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) {
AutoTArray<nsString, 1> params = {
NS_ConvertUTF8toUTF16(aURI->GetSpecOrDefault())};
nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyNoUpgradeException", params,
nsIScriptError::infoFlag, aLoadInfo,
aURI);
return false;
}
// All subresources of an exempt triggering principal are also exempt
ExtContentPolicyType contentType = aLoadInfo->GetExternalContentPolicyType();
if (contentType != ExtContentPolicy::TYPE_DOCUMENT) {
if (!aLoadInfo->TriggeringPrincipal()->IsSystemPrincipal() &&
TestIfPrincipalIsExempt(aLoadInfo->TriggeringPrincipal())) {
return false;
}
}
// We can not upgrade "Save-As" downloads, since we have no way of detecting
// if the upgrade failed (Bug 1674859). For now we will just allow the
// download, since there will still be a visual warning about the download
// being insecure.
if (contentType == ExtContentPolicyType::TYPE_SAVEAS_DOWNLOAD) {
return false;
}
// We can upgrade the request - let's log it to the console
// Appending an 's' to the scheme for the logging. (http -> https)
nsAutoCString scheme;
aURI->GetScheme(scheme);
scheme.AppendLiteral("s");
NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault());
NS_ConvertUTF8toUTF16 reportScheme(scheme);
bool isSpeculative = aLoadInfo->GetExternalContentPolicyType() ==
ExtContentPolicy::TYPE_SPECULATIVE;
AutoTArray<nsString, 2> params = {reportSpec, reportScheme};
nsHTTPSOnlyUtils::LogLocalizedString(
isSpeculative ? "HTTPSOnlyUpgradeSpeculativeConnection"
: "HTTPSOnlyUpgradeRequest",
params, nsIScriptError::warningFlag, aLoadInfo, aURI);
// If the status was not determined before, we now indicate that the request
// will get upgraded, but no event-listener has been registered yet.
if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UNINITIALIZED) {
httpsOnlyStatus ^= nsILoadInfo::HTTPS_ONLY_UNINITIALIZED;
httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED;
aLoadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
}
return true;
}
/* static */
bool nsHTTPSOnlyUtils::ShouldUpgradeWebSocket(nsIURI* aURI,
nsILoadInfo* aLoadInfo) {
// 1. Check if the HTTPS-Only Mode is even enabled, before we do anything else
bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
if (!IsHttpsOnlyModeEnabled(isPrivateWin)) {
return false;
}
// 2. Check for general exceptions
if (OnionException(aURI) || LoopbackOrLocalException(aURI)) {
return false;
}
// 3. Check if NoUpgrade-flag is set in LoadInfo
uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) {
// Let's log to the console, that we didn't upgrade this request
AutoTArray<nsString, 1> params = {
NS_ConvertUTF8toUTF16(aURI->GetSpecOrDefault())};
nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyNoUpgradeException", params,
nsIScriptError::infoFlag, aLoadInfo,
aURI);
return false;
}
// All subresources of an exempt triggering principal are also exempt.
if (!aLoadInfo->TriggeringPrincipal()->IsSystemPrincipal() &&
TestIfPrincipalIsExempt(aLoadInfo->TriggeringPrincipal())) {
return false;
}
// We can upgrade the request - let's log it to the console
// Appending an 's' to the scheme for the logging. (ws -> wss)
nsAutoCString scheme;
aURI->GetScheme(scheme);
scheme.AppendLiteral("s");
NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault());
NS_ConvertUTF8toUTF16 reportScheme(scheme);
AutoTArray<nsString, 2> params = {reportSpec, reportScheme};
nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyUpgradeRequest", params,
nsIScriptError::warningFlag, aLoadInfo,
aURI);
return true;
}
/* static */
bool nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(
nsIURI* aURI, nsILoadInfo* aLoadInfo,
const mozilla::EnumSet<UpgradeDowngradeEndlessLoopOptions>& aOptions) {
// 1. Check if the HTTPS-Only/HTTPS-First is even enabled, before doing
// anything else
bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
bool enforceForHTTPSOnlyMode =
IsHttpsOnlyModeEnabled(isPrivateWin) &&
aOptions.contains(
UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSOnlyMode);
bool enforceForHTTPSFirstMode =
IsHttpsFirstModeEnabled(isPrivateWin) &&
aOptions.contains(
UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSFirstMode);
bool enforceForHTTPSRR =
aOptions.contains(UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSRR);
if (!enforceForHTTPSOnlyMode && !enforceForHTTPSFirstMode &&
!enforceForHTTPSRR) {
return false;
}
// 2. Check if the upgrade downgrade pref even wants us to try to break the
// cycle. In the case that HTTPS RR is presented, we ignore this pref.
if (!mozilla::StaticPrefs::
dom_security_https_only_mode_break_upgrade_downgrade_endless_loop() &&
!enforceForHTTPSRR) {
return false;
}
// 3. If it's not a top-level load, then there is nothing to do here either.
if (aLoadInfo->GetExternalContentPolicyType() !=
ExtContentPolicy::TYPE_DOCUMENT) {
return false;
}
// 4. If the load is exempt, then it's defintely not related to https-only
uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
if ((httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) &&
!enforceForHTTPSRR) {
return false;
}
// 5. If the URI to be loaded is not http, then it's defnitely no endless
// loop caused by https-only.
if (!aURI->SchemeIs("http")) {
return false;
}
nsAutoCString uriHost;
aURI->GetAsciiHost(uriHost);
auto uriAndPrincipalComparator = [&](nsIPrincipal* aPrincipal) {
nsAutoCString principalHost;
aPrincipal->GetAsciiHost(principalHost);
bool checkPath = mozilla::StaticPrefs::
dom_security_https_only_check_path_upgrade_downgrade_endless_loop();
if (!checkPath) {
return uriHost.Equals(principalHost);
}
nsAutoCString uriPath;
nsresult rv = aURI->GetFilePath(uriPath);
if (NS_FAILED(rv)) {
return false;
}
nsAutoCString principalPath;
aPrincipal->GetFilePath(principalPath);
return uriHost.Equals(principalHost) && uriPath.Equals(principalPath);
};
// 6. Check actual redirects. If the Principal that kicked off the
// load/redirect is not https, then it's definitely not a redirect cause by
// https-only. If the scheme of the principal however is https and the
// asciiHost of the URI to be loaded and the asciiHost of the Principal are
// identical, then we are dealing with an upgrade downgrade scenario and we
// have to break the cycle.
if (!aLoadInfo->RedirectChain().IsEmpty()) {
nsCOMPtr<nsIPrincipal> redirectPrincipal;
for (nsIRedirectHistoryEntry* entry : aLoadInfo->RedirectChain()) {
entry->GetPrincipal(getter_AddRefs(redirectPrincipal));
if (redirectPrincipal && redirectPrincipal->SchemeIs("https") &&
uriAndPrincipalComparator(redirectPrincipal)) {
return true;
}
}
} else {
// 6.1 We should only check if this load is triggered by a user gesture
// when the redirect chain is empty, since this information is only useful
// in our case here. When the redirect chain is not empty, this load is
// defnitely triggered by redirection, not a user gesture.
if (aLoadInfo->GetHasValidUserGestureActivation()) {
return false;
}
}
// 7. Meta redirects and JS based redirects (win.location). If the security
// context that triggered the load is not https, then it's defnitely no
// endless loop caused by https-only. If the scheme is http however and the
// asciiHost of the URI to be loaded matches the asciiHost of the Principal,
// then we are dealing with an upgrade downgrade scenario and we have to break
// the cycle.
nsCOMPtr<nsIPrincipal> triggeringPrincipal = aLoadInfo->TriggeringPrincipal();
if (!triggeringPrincipal->SchemeIs("https")) {
return false;
}
return uriAndPrincipalComparator(triggeringPrincipal);
}
/* static */
bool nsHTTPSOnlyUtils::ShouldUpgradeHttpsFirstRequest(nsIURI* aURI,
nsILoadInfo* aLoadInfo) {
// 1. Check if HTTPS-First Mode is enabled
bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
if (!IsHttpsFirstModeEnabled(isPrivateWin) &&
!(aLoadInfo->GetWasSchemelessInput() &&
mozilla::StaticPrefs::dom_security_https_first_schemeless())) {
return false;
}
// 2. HTTPS-First only upgrades top-level loads (and speculative connections)
ExtContentPolicyType contentType = aLoadInfo->GetExternalContentPolicyType();
if (contentType != ExtContentPolicy::TYPE_DOCUMENT &&
contentType != ExtContentPolicy::TYPE_SPECULATIVE) {
return false;
}
// 3. Check for general exceptions
if (OnionException(aURI) || LoopbackOrLocalException(aURI)) {
return false;
}
// 4. Don't upgrade if upgraded previously or exempt from upgrades
uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) {
return false;
}
// 5. HTTPS-First Mode only upgrades default ports - do not upgrade the
// request to https if port is specified and not the default port of 80.
MOZ_ASSERT(aURI->SchemeIs("http"), "how come the request is not 'http'?");
int defaultPortforScheme = NS_GetDefaultPort("http");
// If no port is specified, then the API returns -1 to indicate the default
// port.
int32_t port = 0;
nsresult rv = aURI->GetPort(&port);
NS_ENSURE_SUCCESS(rv, false);
if (port != defaultPortforScheme && port != -1) {
return false;
}
// 6. Do not upgrade form submissions (for now), revisit within
// Bug 1720500: Revisit upgrading form submissions.
if (aLoadInfo->GetIsFormSubmission()) {
return false;
}
// https-first needs to account for breaking upgrade-downgrade endless
// loops at this point because this function is called before we
// check the redirect limit in HttpBaseChannel. If we encounter
// a same-origin server side downgrade from e.g https://example.com
// to http://example.com then we simply not annotating the loadinfo
// and returning false from within this function. Please note that
// the handling for https-only mode is different from https-first mode,
// because https-only mode results in an exception page in case
// we encounter and endless upgrade downgrade loop.
bool isUpgradeDowngradeEndlessLoop = IsUpgradeDowngradeEndlessLoop(
aURI, aLoadInfo,
{UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSFirstMode});
if (isUpgradeDowngradeEndlessLoop) {
return false;
}
// We can upgrade the request - let's log to the console and set the status
// so we know that we upgraded the request.
if (aLoadInfo->GetWasSchemelessInput() &&
!IsHttpsFirstModeEnabled(isPrivateWin)) {
nsAutoCString urlCString;
aURI->GetSpec(urlCString);
NS_ConvertUTF8toUTF16 urlString(urlCString);
AutoTArray<nsString, 1> params = {urlString};
nsHTTPSOnlyUtils::LogLocalizedString("HTTPSFirstSchemeless", params,
nsIScriptError::warningFlag, aLoadInfo,
aURI, true);
mozilla::glean::httpsfirst::upgraded_schemeless.Add();
} else {
nsAutoCString scheme;
aURI->GetScheme(scheme);
scheme.AppendLiteral("s");
NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault());
NS_ConvertUTF8toUTF16 reportScheme(scheme);
bool isSpeculative = contentType == ExtContentPolicy::TYPE_SPECULATIVE;
AutoTArray<nsString, 2> params = {reportSpec, reportScheme};
nsHTTPSOnlyUtils::LogLocalizedString(
isSpeculative ? "HTTPSOnlyUpgradeSpeculativeConnection"
: "HTTPSOnlyUpgradeRequest",
params, nsIScriptError::warningFlag, aLoadInfo, aURI, true);
if (!isSpeculative) {
mozilla::glean::httpsfirst::upgraded.Add();
}
}
// Set flag so we know that we upgraded the request
httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST;
aLoadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
return true;
}
/* static */
already_AddRefed<nsIURI>
nsHTTPSOnlyUtils::PotentiallyDowngradeHttpsFirstRequest(
mozilla::net::DocumentLoadListener* aDocumentLoadListener,
nsresult aStatus) {
nsCOMPtr<nsIChannel> channel = aDocumentLoadListener->GetChannel();
nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
// Only downgrade if we this request was upgraded using HTTPS-First Mode
if (!(httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST)) {
return nullptr;
}
// Once loading is in progress we set that flag so that timeout counter
// measures do not kick in.
loadInfo->SetHttpsOnlyStatus(
httpsOnlyStatus | nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS);
nsresult status = aStatus;
// Since 4xx and 5xx errors return NS_OK instead of NS_ERROR_*, we need
// to check each NS_OK for those errors.
// Only downgrade an NS_OK status if it is an 4xx or 5xx error.
if (NS_SUCCEEDED(aStatus)) {
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel);
// If no httpChannel exists we have nothing to do here.
if (!httpChannel) {
return nullptr;
}
uint32_t responseStatus = 0;
if (NS_FAILED(httpChannel->GetResponseStatus(&responseStatus))) {
return nullptr;
}
// In case we found one 4xx or 5xx error we need to log it later on,
// for that reason we flip the nsresult 'status' from 'NS_OK' to the
// corresponding NS_ERROR_*.
// To do so we convert the response status to an nsresult error
// Every NS_OK that is NOT an 4xx or 5xx error code won't get downgraded.
if (responseStatus >= 400 && responseStatus < 600) {
// HttpProxyResponseToErrorCode() maps 400 and 404 on
// the same error as a 500 status which would lead to no downgrade
// later on. For that reason we explicit filter for 400 and 404 status
// codes to log them correctly and to downgrade them if possible.
switch (responseStatus) {
case 400:
status = NS_ERROR_PROXY_BAD_REQUEST;
break;
case 404:
status = NS_ERROR_PROXY_NOT_FOUND;
break;
default:
status = mozilla::net::HttpProxyResponseToErrorCode(responseStatus);
break;
}
}
if (NS_SUCCEEDED(status)) {
return nullptr;
}
}
// We're only downgrading if it's possible that the error was
// caused by the upgrade.
if (HttpsUpgradeUnrelatedErrorCode(status)) {
return nullptr;
}
nsCOMPtr<nsIURI> uri;
nsresult rv = channel->GetURI(getter_AddRefs(uri));
NS_ENSURE_SUCCESS(rv, nullptr);
nsAutoCString spec;
nsCOMPtr<nsIURI> newURI;
// Only downgrade if the current scheme is (a) https or (b) view-source:https
if (uri->SchemeIs("https")) {
rv = uri->GetSpec(spec);
NS_ENSURE_SUCCESS(rv, nullptr);
rv = NS_NewURI(getter_AddRefs(newURI), spec);
NS_ENSURE_SUCCESS(rv, nullptr);
rv = NS_MutateURI(newURI).SetScheme("http"_ns).Finalize(
getter_AddRefs(newURI));
NS_ENSURE_SUCCESS(rv, nullptr);
} else if (uri->SchemeIs("view-source")) {
nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(uri);
if (!nestedURI) {
return nullptr;
}
nsCOMPtr<nsIURI> innerURI;
rv = nestedURI->GetInnerURI(getter_AddRefs(innerURI));
NS_ENSURE_SUCCESS(rv, nullptr);
if (!innerURI || !innerURI->SchemeIs("https")) {
return nullptr;
}
rv = NS_MutateURI(innerURI).SetScheme("http"_ns).Finalize(
getter_AddRefs(innerURI));
NS_ENSURE_SUCCESS(rv, nullptr);
nsAutoCString innerSpec;
rv = innerURI->GetSpec(innerSpec);
NS_ENSURE_SUCCESS(rv, nullptr);
spec.Append("view-source:");
spec.Append(innerSpec);
rv = NS_NewURI(getter_AddRefs(newURI), spec);
NS_ENSURE_SUCCESS(rv, nullptr);
} else {
return nullptr;
}
// Log downgrade to console
NS_ConvertUTF8toUTF16 reportSpec(uri->GetSpecOrDefault());
AutoTArray<nsString, 1> params = {reportSpec};
nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyFailedDowngradeAgain", params,
nsIScriptError::warningFlag, loadInfo,
uri, true);
// Record telemety
nsDOMNavigationTiming* timing = aDocumentLoadListener->GetTiming();
if (timing) {
mozilla::TimeStamp navigationStart = timing->GetNavigationStartTimeStamp();
if (navigationStart) {
mozilla::TimeDuration duration =
mozilla::TimeStamp::Now() - navigationStart;
bool isPrivateWin =
loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
if (loadInfo->GetWasSchemelessInput() &&
!IsHttpsFirstModeEnabled(isPrivateWin)) {
mozilla::glean::httpsfirst::downgraded_schemeless.Add();
if (timing) {
mozilla::glean::httpsfirst::downgrade_time_schemeless
.AccumulateRawDuration(duration);
}
} else {
mozilla::glean::httpsfirst::downgraded.Add();
if (timing) {
mozilla::glean::httpsfirst::downgrade_time.AccumulateRawDuration(
duration);
}
}
nsresult channelStatus;
channel->GetStatus(&channelStatus);
if (channelStatus == NS_ERROR_NET_TIMEOUT_EXTERNAL) {
if (loadInfo->GetWasSchemelessInput() &&
!nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(isPrivateWin)) {
mozilla::glean::httpsfirst::downgraded_on_timer_schemeless
.AddToNumerator();
} else {
mozilla::glean::httpsfirst::downgraded_on_timer.AddToNumerator();
}
}
}
}
return newURI.forget();
}
/* static */
bool nsHTTPSOnlyUtils::CouldBeHttpsOnlyError(nsIChannel* aChannel,
nsresult aError) {
// If there is no failed channel, then there is nothing to do here.
if (!aChannel) {
return false;
}
// If HTTPS-Only Mode is not enabled, then there is nothing to do here.
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
if (!IsHttpsOnlyModeEnabled(isPrivateWin)) {
return false;
}
// If the load is exempt or did not get upgraded,
// then there is nothing to do here.
uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT ||
httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UNINITIALIZED) {
return false;
}
// If it's one of those errors, then most likely it's not a HTTPS-Only error
// (This list of errors is largely drawn from nsDocShell::DisplayLoadError())
return !HttpsUpgradeUnrelatedErrorCode(aError);
}
/* static */
bool nsHTTPSOnlyUtils::TestIfPrincipalIsExempt(nsIPrincipal* aPrincipal) {
static nsCOMPtr<nsIPermissionManager> sPermMgr;
if (!sPermMgr) {
sPermMgr = mozilla::components::PermissionManager::Service();
mozilla::ClearOnShutdown(&sPermMgr);
}
NS_ENSURE_TRUE(sPermMgr, false);
uint32_t perm;
nsresult rv = sPermMgr->TestExactPermissionFromPrincipal(
aPrincipal, "https-only-load-insecure"_ns, &perm);
NS_ENSURE_SUCCESS(rv, false);
return perm == nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW ||
perm == nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW_SESSION;
}
/* static */
void nsHTTPSOnlyUtils::TestSitePermissionAndPotentiallyAddExemption(
nsIChannel* aChannel) {
NS_ENSURE_TRUE_VOID(aChannel);
// If HTTPS-Only or HTTPS-First Mode is not enabled, then there is nothing to
// do here.
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
bool isHttpsOnly = IsHttpsOnlyModeEnabled(isPrivateWin);
bool isHttpsFirst = IsHttpsFirstModeEnabled(isPrivateWin);
bool isSchemelessHttpsFirst =
(loadInfo->GetWasSchemelessInput() &&
mozilla::StaticPrefs::dom_security_https_first_schemeless());
if (!isHttpsOnly && !isHttpsFirst && !isSchemelessHttpsFirst) {
return;
}
// if it's not a top-level load then there is nothing to here.
ExtContentPolicyType type = loadInfo->GetExternalContentPolicyType();
if (type != ExtContentPolicy::TYPE_DOCUMENT) {
return;
}
// it it's not an http channel, then there is nothing to do here.
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
if (!httpChannel) {
return;
}
nsCOMPtr<nsIPrincipal> principal;
nsresult rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
aChannel, getter_AddRefs(principal));
NS_ENSURE_SUCCESS_VOID(rv);
uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
bool isPrincipalExempt = TestIfPrincipalIsExempt(principal);
if (isPrincipalExempt) {
httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_EXEMPT;
} else {
// We explicitly remove the exemption flag, because this
// function is also consulted after redirects.
httpsOnlyStatus &= ~nsILoadInfo::HTTPS_ONLY_EXEMPT;
}
if (httpsOnlyStatus & nsILoadInfo::HTTPS_FIRST_EXEMPT_NEXT_LOAD &&
isHttpsFirst) {
httpsOnlyStatus &= ~nsILoadInfo::HTTPS_FIRST_EXEMPT_NEXT_LOAD;
httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_EXEMPT;
}
loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
}
/* static */
bool nsHTTPSOnlyUtils::IsSafeToAcceptCORSOrMixedContent(
nsILoadInfo* aLoadInfo) {
// Check if the request is exempt from upgrades
if ((aLoadInfo->GetHttpsOnlyStatus() & nsILoadInfo::HTTPS_ONLY_EXEMPT)) {
return false;
}
// Check if HTTPS-Only Mode is enabled for this request
bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
return nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(isPrivateWin);
}
/* static */
bool nsHTTPSOnlyUtils::HttpsUpgradeUnrelatedErrorCode(nsresult aError) {
return NS_ERROR_UNKNOWN_PROTOCOL == aError ||
NS_ERROR_FILE_NOT_FOUND == aError ||
NS_ERROR_FILE_ACCESS_DENIED == aError ||
NS_ERROR_UNKNOWN_HOST == aError || NS_ERROR_PHISHING_URI == aError ||
NS_ERROR_MALWARE_URI == aError || NS_ERROR_UNWANTED_URI == aError ||
NS_ERROR_HARMFUL_URI == aError || NS_ERROR_CONTENT_CRASHED == aError ||
NS_ERROR_FRAME_CRASHED == aError || NS_ERROR_SUPERFLUOS_AUTH == aError;
}
/* ------ Logging ------ */
/* static */
void nsHTTPSOnlyUtils::LogLocalizedString(const char* aName,
const nsTArray<nsString>& aParams,
uint32_t aFlags,
nsILoadInfo* aLoadInfo, nsIURI* aURI,
bool aUseHttpsFirst) {
nsAutoString logMsg;
nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES,
aName, aParams, logMsg);
LogMessage(logMsg, aFlags, aLoadInfo, aURI, aUseHttpsFirst);
}
/* static */
void nsHTTPSOnlyUtils::LogMessage(const nsAString& aMessage, uint32_t aFlags,
nsILoadInfo* aLoadInfo, nsIURI* aURI,
bool aUseHttpsFirst) {
// do not log to the console if the loadinfo says we should not!
uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE) {
return;
}
// Prepending HTTPS-Only to the outgoing console message
nsString message;
message.Append(aUseHttpsFirst ? u"HTTPS-First Mode: "_ns
: u"HTTPS-Only Mode: "_ns);
message.Append(aMessage);
// Allow for easy distinction in devtools code.
auto category = aUseHttpsFirst ? "HTTPSFirst"_ns : "HTTPSOnly"_ns;
uint64_t windowId = aLoadInfo->GetInnerWindowID();
if (!windowId) {
windowId = aLoadInfo->GetTriggeringWindowId();
}
if (windowId) {
// Send to content console
nsContentUtils::ReportToConsoleByWindowID(message, aFlags, category,
windowId, aURI);
} else {
// Send to browser console
bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
nsContentUtils::LogSimpleConsoleError(message, category, isPrivateWin,
true /* from chrome context */,
aFlags);
}
}
/* ------ Exceptions ------ */
/* static */
bool nsHTTPSOnlyUtils::OnionException(nsIURI* aURI) {
// Onion-host exception can get disabled with a pref
if (mozilla::StaticPrefs::dom_security_https_only_mode_upgrade_onion()) {
return false;
}
nsAutoCString host;
aURI->GetHost(host);
return StringEndsWith(host, ".onion"_ns);
}
/* static */
bool nsHTTPSOnlyUtils::LoopbackOrLocalException(nsIURI* aURI) {
nsAutoCString asciiHost;
nsresult rv = aURI->GetAsciiHost(asciiHost);
NS_ENSURE_SUCCESS(rv, false);
// Let's make a quick check if the host matches these loopback strings
// before we do anything else
if (asciiHost.EqualsLiteral("localhost") || asciiHost.EqualsLiteral("::1")) {
return true;
}
mozilla::net::NetAddr addr;
if (NS_FAILED(addr.InitFromString(asciiHost))) {
return false;
}
// Loopback IPs are always exempt
if (addr.IsLoopbackAddr()) {
return true;
}
// Local IP exception can get disabled with a pref
bool upgradeLocal =
mozilla::StaticPrefs::dom_security_https_only_mode_upgrade_local();
return (!upgradeLocal && addr.IsIPAddrLocal());
}
/* static */
bool nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef(nsIURI* aHTTPSSchemeURI,
nsIURI* aOtherURI,
nsILoadInfo* aLoadInfo) {
// 1. Check if one of parameters is null then webpage can't be loaded yet
// and no further inspections are needed
if (!aHTTPSSchemeURI || !aOtherURI || !aLoadInfo) {
return false;
}
// 2. If the URI to be loaded is not http, then same origin will be detected
// already
if (!mozilla::net::SchemeIsHTTP(aOtherURI)) {
return false;
}
// 3. Check if the HTTPS-Only Mode is even enabled, before we do anything else
bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
if (!IsHttpsOnlyModeEnabled(isPrivateWin) &&
!IsHttpsFirstModeEnabled(isPrivateWin)) {
return false;
}
// 4. If the load is exempt, then it's defintely not related to https-only
uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) {
return false;
}
// 5. Create a new target URI with 'https' instead of 'http' and compare it
// to the current URI
int32_t port = 0;
nsresult rv = aOtherURI->GetPort(&port);
NS_ENSURE_SUCCESS(rv, false);
// a port of -1 indicates the default port, hence we upgrade from port 80 to
// port 443
// otherwise we keep the port.
if (port == -1) {
port = NS_GetDefaultPort("https");
}
nsCOMPtr<nsIURI> newHTTPSchemeURI;
rv = NS_MutateURI(aOtherURI)
.SetScheme("https"_ns)
.SetPort(port)
.Finalize(newHTTPSchemeURI);
NS_ENSURE_SUCCESS(rv, false);
bool uriEquals = false;
if (NS_FAILED(
aHTTPSSchemeURI->EqualsExceptRef(newHTTPSchemeURI, &uriEquals))) {
return false;
}
return uriEquals;
}
/* static */
uint32_t nsHTTPSOnlyUtils::GetStatusForSubresourceLoad(
uint32_t aHttpsOnlyStatus) {
return aHttpsOnlyStatus & ~nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST;
}
/////////////////////////////////////////////////////////////////////
// Implementation of TestHTTPAnswerRunnable
NS_IMPL_ISUPPORTS_INHERITED(TestHTTPAnswerRunnable, mozilla::Runnable,
nsIStreamListener, nsIInterfaceRequestor,
nsITimerCallback)
TestHTTPAnswerRunnable::TestHTTPAnswerRunnable(
nsIURI* aURI, mozilla::net::DocumentLoadListener* aDocumentLoadListener)
: mozilla::Runnable("TestHTTPAnswerRunnable"),
mURI(aURI),
mDocumentLoadListener(aDocumentLoadListener) {}
/* static */
bool TestHTTPAnswerRunnable::IsBackgroundRequestRedirected(
nsIHttpChannel* aChannel) {
// If there is no background request (aChannel), then there is nothing
// to do here.
if (!aChannel) {
return false;
}
// If the request was not redirected, then there is nothing to do here.
nsCOMPtr<nsILoadInfo> loadinfo = aChannel->LoadInfo();
if (loadinfo->RedirectChain().IsEmpty()) {
return false;
}
// If the final URI is not targeting an https scheme, then we definitely not
// dealing with a 'same-origin' redirect.
nsCOMPtr<nsIURI> finalURI;
nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI));
NS_ENSURE_SUCCESS(rv, false);
if (!finalURI->SchemeIs("https")) {
return false;
}
// If the background request was not http, then there is nothing to do here.
nsCOMPtr<nsIPrincipal> firstURIPrincipal;
loadinfo->RedirectChain()[0]->GetPrincipal(getter_AddRefs(firstURIPrincipal));
if (!firstURIPrincipal || !firstURIPrincipal->SchemeIs("http")) {
return false;
}
// By now we have verified that the inital background request was http and
// that the redirected scheme is https. We want to find the following case
// where the background channel redirects to the https version of the
// top-level request.
// --> background channel: http://example.com
// |--> redirects to: https://example.com
// Now we have to check that the hosts are 'same-origin'.
nsAutoCString redirectHost;
nsAutoCString finalHost;
firstURIPrincipal->GetAsciiHost(redirectHost);
finalURI->GetAsciiHost(finalHost);
return finalHost.Equals(redirectHost);
}
NS_IMETHODIMP
TestHTTPAnswerRunnable::OnStartRequest(nsIRequest* aRequest) {
// If the request status is not OK, it means it encountered some
// kind of error in which case we do not want to do anything.
nsresult requestStatus;
aRequest->GetStatus(&requestStatus);
if (requestStatus != NS_OK) {
return NS_OK;
}
// Check if the original top-level channel which https-only is trying
// to upgrade is already in progress or if the channel is an auth channel.
// If it is in progress or Auth is in progress, then all good, if not
// then let's cancel that channel so we can dispaly the exception page.
nsCOMPtr<nsIChannel> docChannel = mDocumentLoadListener->GetChannel();
nsCOMPtr<nsIHttpChannel> httpsOnlyChannel = do_QueryInterface(docChannel);
if (httpsOnlyChannel) {
nsCOMPtr<nsILoadInfo> loadInfo = httpsOnlyChannel->LoadInfo();
uint32_t topLevelLoadInProgress =
loadInfo->GetHttpsOnlyStatus() &
nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS;
nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal =
do_QueryInterface(httpsOnlyChannel);
bool isAuthChannel = false;
mozilla::Unused << httpChannelInternal->GetIsAuthChannel(&isAuthChannel);
// some server configurations need a long time to respond to an https
// connection, but also redirect any http connection to the https version of
// it. If the top-level load has not started yet, but the http background
// request redirects to https, then do not show the error page, but keep
// waiting for the https response of the upgraded top-level request.
if (!topLevelLoadInProgress) {
nsCOMPtr<nsIHttpChannel> backgroundHttpChannel =
do_QueryInterface(aRequest);
topLevelLoadInProgress =
IsBackgroundRequestRedirected(backgroundHttpChannel);
}
if (!topLevelLoadInProgress && !isAuthChannel) {
// Only really cancel the original top-level channel if it's
// status is still NS_OK, otherwise it might have already
// encountered some other error and was cancelled.
nsresult httpsOnlyChannelStatus;
httpsOnlyChannel->GetStatus(&httpsOnlyChannelStatus);
if (httpsOnlyChannelStatus == NS_OK) {
httpsOnlyChannel->Cancel(NS_ERROR_NET_TIMEOUT_EXTERNAL);
}
}
}
// Cancel this http request because it has reached the end of it's
// lifetime at this point.
aRequest->Cancel(NS_ERROR_ABORT);
return NS_ERROR_ABORT;
}
NS_IMETHODIMP
TestHTTPAnswerRunnable::OnDataAvailable(nsIRequest* aRequest,
nsIInputStream* aStream,
uint64_t aOffset, uint32_t aCount) {
// TestHTTPAnswerRunnable only cares about ::OnStartRequest which
// will also cancel the request, so we should in fact never even
// get here.
MOZ_ASSERT(false, "how come we get to ::OnDataAvailable");
return NS_OK;
}
NS_IMETHODIMP
TestHTTPAnswerRunnable::OnStopRequest(nsIRequest* aRequest,
nsresult aStatusCode) {
// TestHTTPAnswerRunnable only cares about ::OnStartRequest
return NS_OK;
}
NS_IMETHODIMP
TestHTTPAnswerRunnable::GetInterface(const nsIID& aIID, void** aResult) {
return QueryInterface(aIID, aResult);
}
NS_IMETHODIMP
TestHTTPAnswerRunnable::Run() {
// Wait N milliseconds to give the original https request a heads start
// before firing up this http request in the background. By default the
// timer is set to 3 seconds. If the https request has not received
// any signal from the server during that time, than it's almost
// certain the upgraded request will result in time out.
uint32_t background_timer_ms = mozilla::StaticPrefs::
dom_security_https_only_fire_http_request_background_timer_ms();
return NS_NewTimerWithCallback(getter_AddRefs(mTimer), this,
background_timer_ms, nsITimer::TYPE_ONE_SHOT);
}
NS_IMETHODIMP
TestHTTPAnswerRunnable::Notify(nsITimer* aTimer) {
if (mTimer) {
mTimer->Cancel();
mTimer = nullptr;
}
// If the original channel has already started loading at this point
// then there is no need to do the dance.
nsCOMPtr<nsIChannel> origChannel = mDocumentLoadListener->GetChannel();
nsCOMPtr<nsILoadInfo> origLoadInfo = origChannel->LoadInfo();
uint32_t origHttpsOnlyStatus = origLoadInfo->GetHttpsOnlyStatus();
uint32_t topLevelLoadInProgress =
origHttpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS;
uint32_t downloadInProgress =
origHttpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_DOWNLOAD_IN_PROGRESS;
if (topLevelLoadInProgress || downloadInProgress) {
return NS_OK;
}
mozilla::OriginAttributes attrs = origLoadInfo->GetOriginAttributes();
RefPtr<nsIPrincipal> nullPrincipal = mozilla::NullPrincipal::Create(attrs);
uint32_t loadFlags =
nsIRequest::LOAD_ANONYMOUS | nsIRequest::INHIBIT_CACHING |
nsIRequest::INHIBIT_PERSISTENT_CACHING | nsIRequest::LOAD_BYPASS_CACHE |
nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
// No need to connect to the URI including the path because we only care about
// the round trip time if a server responds to an http request.
nsCOMPtr<nsIURI> backgroundChannelURI;
nsAutoCString prePathStr;
nsresult rv = mURI->GetPrePath(prePathStr);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = NS_NewURI(getter_AddRefs(backgroundChannelURI), prePathStr);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// we are using TYPE_OTHER because TYPE_DOCUMENT might have side effects
nsCOMPtr<nsIChannel> testHTTPChannel;
rv = NS_NewChannel(getter_AddRefs(testHTTPChannel), backgroundChannelURI,
nullPrincipal,
nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
nsIContentPolicy::TYPE_OTHER, nullptr, nullptr, nullptr,
nullptr, loadFlags);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// We have exempt that load from HTTPS-Only to avoid getting upgraded
// to https as well. Additonally let's not log that request to the console
// because it might confuse end users.
nsCOMPtr<nsILoadInfo> loadInfo = testHTTPChannel->LoadInfo();
uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_EXEMPT |
nsILoadInfo::HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE |
nsILoadInfo::HTTPS_ONLY_BYPASS_ORB;
loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
testHTTPChannel->SetNotificationCallbacks(this);
testHTTPChannel->AsyncOpen(this);
return NS_OK;
}