summaryrefslogtreecommitdiffstats
path: root/toolkit/components/httpsonlyerror
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/httpsonlyerror')
-rw-r--r--toolkit/components/httpsonlyerror/content/errorpage.html79
-rw-r--r--toolkit/components/httpsonlyerror/content/errorpage.js134
-rw-r--r--toolkit/components/httpsonlyerror/content/secure-broken.svg4
-rw-r--r--toolkit/components/httpsonlyerror/jar.mn8
-rw-r--r--toolkit/components/httpsonlyerror/moz.build12
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/browser.toml17
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/browser_errorpage.js191
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/browser_errorpage_timeout.js53
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/browser_errorpage_www_suggestion.js80
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/browser_exception.js152
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/browser_fpi_nested_uri.js46
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/file_errorpage_timeout_server.sjs15
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/file_errorpage_www_suggestion.html12
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/file_upgrade_insecure_server.sjs108
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/head.js99
15 files changed, 1010 insertions, 0 deletions
diff --git a/toolkit/components/httpsonlyerror/content/errorpage.html b/toolkit/components/httpsonlyerror/content/errorpage.html
new file mode 100644
index 0000000000..ffa92880f6
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/content/errorpage.html
@@ -0,0 +1,79 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome:; object-src 'none'"
+ />
+ <meta name="color-scheme" content="light dark" />
+ <link
+ rel="stylesheet"
+ href="chrome://global/skin/in-content/info-pages.css"
+ />
+ <link
+ rel="stylesheet"
+ href="chrome://global/skin/aboutHttpsOnlyError.css"
+ />
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="toolkit/about/aboutHttpsOnlyError.ftl" />
+ <!-- If the location of the favicon is changed here, the FAVICON_ERRORPAGE_URL symbol in
+ toolkit/components/places/src/nsFaviconService.h should be updated. -->
+ <link rel="icon" id="favicon" href="chrome://global/skin/icons/info.svg" />
+ <title data-l10n-id="about-httpsonly-title-site-not-available"></title>
+ </head>
+ <body>
+ <main class="container">
+ <div class="title">
+ <h2 data-l10n-id="about-httpsonly-title-alert"></h2>
+ <h1
+ class="title-text"
+ data-l10n-id="about-httpsonly-title-site-not-available"
+ ></h1>
+ </div>
+ <p
+ id="insecure-explanation-unavailable"
+ data-l10n-id="about-httpsonly-explanation-unavailable2"
+ data-l10n-args='{"websiteUrl": ""}'
+ ></p>
+ <p id="learn-more-container">
+ <a
+ id="learnMoreLink"
+ target="_blank"
+ data-l10n-id="about-httpsonly-link-learn-more"
+ ></a>
+ </p>
+
+ <b data-l10n-id="about-httpsonly-explanation-question"></b>
+ <ul>
+ <li data-l10n-id="about-httpsonly-explanation-nosupport"></li>
+ <li data-l10n-id="about-httpsonly-explanation-risk"></li>
+ </ul>
+
+ <p
+ id="explanation-continue"
+ data-l10n-id="about-httpsonly-explanation-continue"
+ ></p>
+ <div class="button-container">
+ <button
+ id="goBack"
+ class="primary"
+ data-l10n-id="about-httpsonly-button-go-back"
+ ></button>
+ <button
+ id="openInsecure"
+ data-l10n-id="about-httpsonly-button-continue-to-site"
+ inert
+ ></button>
+ </div>
+ <div class="suggestion-box" hidden>
+ <h2 data-l10n-id="about-httpsonly-suggestion-box-header"></h2>
+ </div>
+ </main>
+ <script src="chrome://global/content/httpsonlyerror/errorpage.js"></script>
+ </body>
+</html>
diff --git a/toolkit/components/httpsonlyerror/content/errorpage.js b/toolkit/components/httpsonlyerror/content/errorpage.js
new file mode 100644
index 0000000000..f2933252ba
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/content/errorpage.js
@@ -0,0 +1,134 @@
+/* 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/. */
+
+/* eslint-env mozilla/remote-page */
+
+"use strict";
+
+const searchParams = new URLSearchParams(document.documentURI.split("?")[1]);
+
+function initPage() {
+ if (!searchParams.get("e")) {
+ document.getElementById("error").remove();
+ }
+
+ const explanation1 = document.getElementById(
+ "insecure-explanation-unavailable"
+ );
+
+ const pageUrl = new URL(window.location.href.replace(/^view-source:/, ""));
+
+ document.l10n.setAttributes(
+ explanation1,
+ "about-httpsonly-explanation-unavailable2",
+ { websiteUrl: pageUrl.host }
+ );
+
+ const baseSupportURL = RPMGetFormatURLPref("app.support.baseURL");
+ document
+ .getElementById("learnMoreLink")
+ .setAttribute("href", baseSupportURL + "https-only-prefs");
+
+ document
+ .getElementById("openInsecure")
+ .addEventListener("click", onOpenInsecureButtonClick);
+
+ const delay = RPMGetIntPref("security.dialog_enable_delay", 1000);
+ setTimeout(() => {
+ document.getElementById("openInsecure").removeAttribute("inert");
+ }, delay);
+
+ if (window.top == window) {
+ document
+ .getElementById("goBack")
+ .addEventListener("click", onReturnButtonClick);
+ addAutofocus("#goBack", "beforeend");
+ } else {
+ document.getElementById("goBack").remove();
+ }
+
+ const isTopLevel = window.top == window;
+ const hasWWWPrefix = pageUrl.href.startsWith("https://www.");
+ if (isTopLevel && !hasWWWPrefix) {
+ // HTTPS-Only generally simply replaces http: with https:;
+ // here we additionally try to add www and see if that allows to upgrade the connection if it is top level
+
+ window.addEventListener("pingSecureWWWLinkSuccess", () => {
+ activateSuggestionBox();
+ displayWWWSuggestion(pageUrl.host);
+ });
+
+ // try to ping secure www link in the AboutHttpsOnlyErrorChild
+ RPMTryPingSecureWWWLink();
+ }
+}
+
+/* Suggestion Box */
+
+function activateSuggestionBox() {
+ const suggestionBox = document.querySelector(".suggestion-box");
+ suggestionBox.hidden = false;
+}
+
+function displayWWWSuggestion(aURL) {
+ const suggestionBox = document.querySelector(".suggestion-box");
+ const suggestionWWWText = document.createElement("p");
+ const suggestionWWWButton = document.createElement("button");
+ const suggestionButtonContainer = document.createElement("div");
+
+ document.l10n.setAttributes(
+ suggestionWWWText,
+ "about-httpsonly-suggestion-box-www-text",
+ { websiteUrl: aURL }
+ );
+
+ suggestionWWWButton.setAttribute("id", "openWWW");
+ document.l10n.setAttributes(
+ suggestionWWWButton,
+ "about-httpsonly-suggestion-box-www-button",
+ { websiteUrl: aURL }
+ );
+ suggestionWWWButton.addEventListener("click", openSecureWWWButtonClick);
+
+ suggestionButtonContainer.classList.add("button-container");
+
+ suggestionBox.appendChild(suggestionWWWText);
+ suggestionButtonContainer.appendChild(suggestionWWWButton);
+ suggestionBox.appendChild(suggestionButtonContainer);
+}
+
+/* Button Events */
+
+function openSecureWWWButtonClick() {
+ RPMOpenSecureWWWLink();
+}
+
+function onOpenInsecureButtonClick() {
+ document.reloadWithHttpsOnlyException();
+}
+
+function onReturnButtonClick() {
+ RPMSendAsyncMessage("goBack");
+}
+
+/* Utils */
+
+function addAutofocus(selector, position = "afterbegin") {
+ if (window.top != window) {
+ return;
+ }
+ var button = document.querySelector(selector);
+ var parent = button.parentNode;
+ button.remove();
+ button.setAttribute("autofocus", "true");
+ parent.insertAdjacentElement(position, button);
+}
+
+/* Initialize Page */
+
+initPage();
+// Dispatch this event so tests can detect that we finished loading the error page.
+// We're using the same event name as neterror because BrowserTestUtils.sys.mjs relies on that.
+let event = new CustomEvent("AboutNetErrorLoad", { bubbles: true });
+document.dispatchEvent(event);
diff --git a/toolkit/components/httpsonlyerror/content/secure-broken.svg b/toolkit/components/httpsonlyerror/content/secure-broken.svg
new file mode 100644
index 0000000000..417d17b3cc
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/content/secure-broken.svg
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="context-fill #424e5a"><path d="M18.75 9.977h-.727L6 22h12.75A2.25 2.25 0 0 0 21 19.75v-7.523a2.25 2.25 0 0 0-2.25-2.25zm-9.75 0V7a3 3 0 0 1 6 0v1.5l2.838-2.838A5.994 5.994 0 0 0 6 7v2.977h-.75A2.25 2.25 0 0 0 3 12.227v7.523a2.224 2.224 0 0 0 .105.645L13.523 9.977z"></path><path d="M2.5 23a1.5 1.5 0 0 1-1.061-2.561l19-19A1.5 1.5 0 0 1 22.56 3.56l-19 19A1.5 1.5 0 0 1 2.5 23z" fill="#ff0039"></path></svg>
diff --git a/toolkit/components/httpsonlyerror/jar.mn b/toolkit/components/httpsonlyerror/jar.mn
new file mode 100644
index 0000000000..f5b5c42030
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/jar.mn
@@ -0,0 +1,8 @@
+# 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/.
+
+toolkit.jar:
+ content/global/httpsonlyerror/errorpage.html (content/errorpage.html)
+ content/global/httpsonlyerror/errorpage.js (content/errorpage.js)
+ content/global/httpsonlyerror/secure-broken.svg (content/secure-broken.svg)
diff --git a/toolkit/components/httpsonlyerror/moz.build b/toolkit/components/httpsonlyerror/moz.build
new file mode 100644
index 0000000000..b8bb635db4
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Security")
diff --git a/toolkit/components/httpsonlyerror/tests/browser/browser.toml b/toolkit/components/httpsonlyerror/tests/browser/browser.toml
new file mode 100644
index 0000000000..5bbad48226
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/browser.toml
@@ -0,0 +1,17 @@
+[DEFAULT]
+support-files = ["head.js"]
+
+["browser_errorpage.js"]
+skip-if = ["os == 'linux' && asan && !debug"] # Bug 1658616
+
+["browser_errorpage_timeout.js"]
+support-files = ["file_errorpage_timeout_server.sjs"]
+
+["browser_errorpage_www_suggestion.js"]
+support-files = ["file_errorpage_www_suggestion.html"]
+skip-if = ["os == 'android'"] # no https-only errorpage support in android
+
+["browser_exception.js"]
+support-files = ["file_upgrade_insecure_server.sjs"]
+
+["browser_fpi_nested_uri.js"]
diff --git a/toolkit/components/httpsonlyerror/tests/browser/browser_errorpage.js b/toolkit/components/httpsonlyerror/tests/browser/browser_errorpage.js
new file mode 100644
index 0000000000..43f46699c2
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/browser_errorpage.js
@@ -0,0 +1,191 @@
+/* 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/. */
+
+"use strict";
+
+const SECURE_PAGE = "https://example.com/";
+const GOOD_PAGE = "http://example.com/";
+const BAD_CERT = "http://expired.example.com/";
+const UNKNOWN_ISSUER = "http://self-signed.example.com/";
+
+const { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+);
+
+add_task(async function () {
+ info("Check that the error pages shows up");
+
+ await Promise.all([
+ testPageWithURI(
+ GOOD_PAGE,
+ "Should not show error page on upgradeable website.",
+ false
+ ),
+ testPageWithURI(
+ BAD_CERT,
+ "Should show error page on bad-certificate error.",
+ true
+ ),
+ testPageWithURI(
+ UNKNOWN_ISSUER,
+ "Should show error page on unkown-issuer error.",
+ true
+ ),
+ ]);
+});
+
+add_task(async function () {
+ info("Check that the go-back button returns to previous page");
+
+ // Test with and without being in an iFrame
+ for (let useFrame of [false, true]) {
+ let tab = await openErrorPage(BAD_CERT, useFrame);
+ let browser = tab.linkedBrowser;
+
+ is(
+ browser.webNavigation.canGoBack,
+ false,
+ "!webNavigation.canGoBack should be false."
+ );
+ is(
+ browser.webNavigation.canGoForward,
+ false,
+ "webNavigation.canGoForward should be false."
+ );
+
+ // Populate the shistory entries manually, since it happens asynchronously
+ // and the following tests will be too soon otherwise.
+ await TabStateFlusher.flush(browser);
+ let { entries } = JSON.parse(SessionStore.getTabState(tab));
+ is(entries.length, 1, "There should be 1 shistory entry.");
+
+ let bc = browser.browsingContext;
+ if (useFrame) {
+ bc = bc.children[0];
+ }
+
+ if (useFrame) {
+ await SpecialPowers.spawn(bc, [], async function () {
+ let returnButton = content.document.getElementById("goBack");
+ is(
+ returnButton,
+ null,
+ "Return-button should not be present in iFrame."
+ );
+ });
+ } else {
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ "about:home"
+ );
+ await SpecialPowers.spawn(bc, [], async function () {
+ let returnButton = content.document.getElementById("goBack");
+ is(
+ returnButton.getAttribute("autofocus"),
+ "true",
+ "Return-button should have focus."
+ );
+ returnButton.click();
+ });
+
+ await locationChangePromise;
+
+ is(browser.webNavigation.canGoBack, true, "webNavigation.canGoBack");
+ is(
+ browser.webNavigation.canGoForward,
+ false,
+ "!webNavigation.canGoForward"
+ );
+ is(gBrowser.currentURI.spec, "about:home", "Went back");
+ }
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+add_task(async function () {
+ info("Check that the go-back button returns to about:home");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, SECURE_PAGE);
+ let browser = gBrowser.selectedBrowser;
+
+ let errorPageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ BrowserTestUtils.startLoadingURIString(browser, BAD_CERT);
+ await errorPageLoaded;
+
+ is(
+ browser.webNavigation.canGoBack,
+ true,
+ "webNavigation.canGoBack should be true before navigation."
+ );
+ is(
+ browser.webNavigation.canGoForward,
+ false,
+ "webNavigation.canGoForward should be false before navigation."
+ );
+
+ // Populate the shistory entries manually, since it happens asynchronously
+ // and the following tests will be too soon otherwise.
+ await TabStateFlusher.flush(browser);
+ let { entries } = JSON.parse(SessionStore.getTabState(tab));
+ is(entries.length, 2, "There should be 1 shistory entries.");
+
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow",
+ true
+ );
+
+ // Click on "go back" Button
+ await SpecialPowers.spawn(browser, [], async function () {
+ let returnButton = content.document.getElementById("goBack");
+ returnButton.click();
+ });
+ await pageShownPromise;
+
+ is(
+ browser.webNavigation.canGoBack,
+ false,
+ "webNavigation.canGoBack should be false after navigation."
+ );
+ is(
+ browser.webNavigation.canGoForward,
+ true,
+ "webNavigation.canGoForward should be true after navigation."
+ );
+ is(
+ gBrowser.currentURI.spec,
+ SECURE_PAGE,
+ "Should go back to previous page after button click."
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Utils
+
+async function testPageWithURI(uri, message, expect) {
+ // Open new Tab with URI
+ let tab;
+ if (expect) {
+ tab = await openErrorPage(uri, false);
+ } else {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri, true);
+ }
+
+ // Check if HTTPS-Only Error-Page loaded instead
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(
+ browser,
+ [message, expect],
+ function (message, expect) {
+ const doc = content.document;
+ let result = doc.documentURI.startsWith("about:httpsonlyerror");
+ is(result, expect, message);
+ }
+ );
+
+ // Close tab again
+ BrowserTestUtils.removeTab(tab);
+}
diff --git a/toolkit/components/httpsonlyerror/tests/browser/browser_errorpage_timeout.js b/toolkit/components/httpsonlyerror/tests/browser/browser_errorpage_timeout.js
new file mode 100644
index 0000000000..853d93694a
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/browser_errorpage_timeout.js
@@ -0,0 +1,53 @@
+/* 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/. */
+
+"use strict";
+
+// We need to request longer timeout because HTTPS-Only Mode sends the
+// backround http request with a delay of N milliseconds before the
+// actual load gets cancelled.
+requestLongerTimeout(5);
+
+const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+const TEST_PATH_HTTPS = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const TIMEOUT_PAGE_URI_HTTP =
+ TEST_PATH_HTTP + "file_errorpage_timeout_server.sjs";
+const TIMEOUT_PAGE_URI_HTTPS =
+ TEST_PATH_HTTPS + "file_errorpage_timeout_server.sjs";
+
+add_task(async function avoid_timeout_and_show_https_only_error_page() {
+ await BrowserTestUtils.withNewTab("about:blank", async function (browser) {
+ let loaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false, // includeSubFrames = false, no need to includeSubFrames
+ TIMEOUT_PAGE_URI_HTTPS, // Wait for upgraded page to timeout
+ true // maybeErrorPage = true, because we need the error page to appear
+ );
+ BrowserTestUtils.startLoadingURIString(browser, TIMEOUT_PAGE_URI_HTTP);
+ await loaded;
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ const doc = content.document;
+ let errorPage = doc.body.innerHTML;
+ // It's possible that fluent has not been translated when running in
+ // chaos mode, hence let's rather use an element id for verification
+ // that the https-only mode error page has loaded.
+ ok(
+ errorPage.includes("about-httpsonly-button-continue-to-site"),
+ "Potential time-out in https-only mode should cause error page to appear!"
+ );
+ // Verify that the right title is set.
+ ok(
+ errorPage.includes("about-httpsonly-title-site-not-available"),
+ "Potential time-out in https-only mode should cause error page to appear with right title!"
+ );
+ });
+ });
+});
diff --git a/toolkit/components/httpsonlyerror/tests/browser/browser_errorpage_www_suggestion.js b/toolkit/components/httpsonlyerror/tests/browser/browser_errorpage_www_suggestion.js
new file mode 100644
index 0000000000..7e3eac9817
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/browser_errorpage_www_suggestion.js
@@ -0,0 +1,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/. */
+
+"use strict";
+requestLongerTimeout(2);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ ""
+);
+const HTML_PATH = "/file_errorpage_www_suggestion.html";
+const KICK_OF_REQUEST_WITH_SUGGESTION =
+ "http://suggestion-example.com" + TEST_PATH + HTML_PATH;
+
+add_task(async function () {
+ info("Check that the www button shows up and leads to a secure www page");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_only_mode", true],
+ ["dom.security.https_only_mode_send_http_background_request", false],
+ ["dom.security.https_only_mode_error_page_user_suggestions", true],
+ ],
+ });
+
+ let browser = gBrowser.selectedBrowser;
+ let errorPageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ KICK_OF_REQUEST_WITH_SUGGESTION
+ );
+ await errorPageLoaded;
+
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow",
+ true
+ );
+
+ // There's an arbitrary interval of 2 seconds in which the background
+ // request for the www page is made. we wait this out to ensure the
+ // www button has shown up.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(c => setTimeout(c, 2000));
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ let innerHTML = doc.body.innerHTML;
+ let errorPageL10nId = "about-httpsonly-title-alert";
+ let suggestionBoxL10nId = "about-httpsonly-suggestion-box-www-text";
+
+ ok(innerHTML.includes(errorPageL10nId), "the error page should show up");
+ ok(doc.documentURI.startsWith("about:httpsonlyerror"));
+ ok(
+ innerHTML.includes(suggestionBoxL10nId),
+ "the suggestion box should show up"
+ );
+
+ // click on www button
+ let wwwButton = content.document.getElementById("openWWW");
+ Assert.notStrictEqual(wwwButton, null, "The www Button should be shown");
+
+ if (!wwwButton) {
+ ok(false, "We should not be here");
+ } else {
+ wwwButton.click();
+ }
+ });
+ await pageShownPromise;
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ let innerHTML = doc.body.innerHTML;
+ ok(
+ innerHTML.includes("You are now on the secure www. page"),
+ "The secure page should be reached after clicking the button"
+ );
+ ok(doc.documentURI.startsWith("https://www."), "Page should be secure www");
+ });
+});
diff --git a/toolkit/components/httpsonlyerror/tests/browser/browser_exception.js b/toolkit/components/httpsonlyerror/tests/browser/browser_exception.js
new file mode 100644
index 0000000000..7cf98b467f
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/browser_exception.js
@@ -0,0 +1,152 @@
+/* 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/. */
+
+"use strict";
+
+const ROOT_PATH = getRootDirectory(gTestPath);
+const EXPIRED_ROOT_PATH = ROOT_PATH.replace(
+ "chrome://mochitests/content",
+ "http://supports-insecure.expired.example.com"
+);
+const SECURE_ROOT_PATH = ROOT_PATH.replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+const INSECURE_ROOT_PATH = ROOT_PATH.replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+// This is how this test works:
+//
+// +----[REQUEST] https://file_upgrade_insecure_server.sjs?queryresult
+// |
+// | +-[REQUEST] http://file_upgrade_insecure_server.sjs?content
+// | | -> Internal HTTPS redirect
+// | |
+// | +>[RESPONSE] Expired Certificate Response
+// | -> HTTPS-Only Mode Error Page shows up
+// | -> Click exception button
+// |
+// | +-[REQUEST] http://file_upgrade_insecure_server.sjs?content
+// | |
+// | +>[RESPONSE] Webpage with a bunch of sub-resources
+// | -> http://file_upgrade_insecure_ser^er.sjs?img
+// | -> http://file_upgrade_insecure_server.sjs?xhr
+// | -> http://file_upgrade_insecure_server.sjs?iframe
+// | -> etc.
+// |
+// +--->[RESPONSE] List of all recorded requests and whether they were loaded
+// with HTTP or not (eg.: img-ok, xhr-ok, iframe-error, ...)
+
+add_task(async function () {
+ const testCases = ["default", "private", "firstpartyisolation"];
+ for (let i = 0; i < testCases.length; i++) {
+ // Call sjs-file with setup query-string and store promise
+ let expectedQueries = new Set([
+ "content",
+ "img",
+ "iframe",
+ "xhr",
+ "nestedimg",
+ ]);
+
+ const filesLoaded = setupFileServer();
+ // Since we don't know when the server has saved all it's variables,
+ // let's wait a bit before reloading the page.
+ await new Promise(resolve => executeSoon(resolve));
+
+ // Create a new private window but reuse the normal one.
+ let privateWindow = false;
+ if (testCases[i] === "private") {
+ privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ } else if (testCases[i] === "firstpartyisolation") {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.firstparty.isolate", true]],
+ });
+ }
+
+ // Create new tab with sjs-file requesting content.
+ // "supports-insecure.expired.example.com" responds to http and https but
+ // with an expired certificate
+ let tab = await openErrorPage(
+ `${EXPIRED_ROOT_PATH}file_upgrade_insecure_server.sjs?content`,
+ false,
+ privateWindow
+ );
+ let browser = tab.linkedBrowser;
+
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow",
+ true
+ );
+
+ await waitForAndClickOpenInsecureButton(browser);
+
+ await pageShownPromise;
+
+ // Check if the original page got loaded with http this time
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ ok(
+ !doc.documentURI.startsWith("http://expired.example.com"),
+ "Page should load normally after exception button was clicked."
+ );
+ });
+
+ // Wait for initial sjs request to resolve
+ let results = await filesLoaded;
+
+ for (let resultIndex in results) {
+ const response = results[resultIndex];
+ // A response looks either like this "iframe-ok" or "[key]-[result]"
+ const [key, result] = response.split("-", 2);
+ // try to find the expected result within the results array
+ if (expectedQueries.has(key)) {
+ expectedQueries.delete(key);
+ is(result, "ok", `Request '${key}' should be loaded with HTTP.'`);
+ } else {
+ ok(false, `Unexpected response from server (${response})`);
+ }
+ }
+
+ // Clean up permissions, tab and potentially preferences
+ Services.perms.removeAll();
+
+ if (testCases[i] === "firstpartyisolation") {
+ await SpecialPowers.popPrefEnv();
+ }
+
+ if (privateWindow) {
+ await BrowserTestUtils.closeWindow(privateWindow);
+ } else {
+ gBrowser.removeCurrentTab();
+ }
+ }
+});
+
+function setupFileServer() {
+ // We initialize the upgrade-server with the queryresult query-string.
+ // We'll get a response once all files have been requested and then
+ // can see if they have been requested with http.
+ return new Promise((resolve, reject) => {
+ var xhrRequest = new XMLHttpRequest();
+ xhrRequest.open(
+ "GET",
+ `${SECURE_ROOT_PATH}file_upgrade_insecure_server.sjs?queryresult=${INSECURE_ROOT_PATH}`
+ );
+ xhrRequest.onload = function (e) {
+ var results = xhrRequest.responseText.split(",");
+ resolve(results);
+ };
+ xhrRequest.onerror = e => {
+ ok(false, "Could not query results from server (" + e.message + ")");
+ reject();
+ };
+ xhrRequest.send();
+ });
+}
diff --git a/toolkit/components/httpsonlyerror/tests/browser/browser_fpi_nested_uri.js b/toolkit/components/httpsonlyerror/tests/browser/browser_fpi_nested_uri.js
new file mode 100644
index 0000000000..45bf718f3f
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/browser_fpi_nested_uri.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that a nested URI (in this case `view-source:`) does not result
+// in a redirect loop when HTTPS-Only and First Party Isolation are
+// enabled (Bug 1855734).
+
+const INSECURE_VIEW_SOURCE_URL = "view-source:http://123.123.123.123/";
+
+function promiseIsErrorPage() {
+ return new Promise(resolve => {
+ BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser).then(() =>
+ resolve(true)
+ );
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() =>
+ resolve(false)
+ );
+ });
+}
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.security.https_only_mode", true],
+ ["dom.security.https_only_mode.upgrade_local", true],
+ ["privacy.firstparty.isolate", true],
+ ],
+ });
+
+ let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ info(`Starting to load ${INSECURE_VIEW_SOURCE_URL}`);
+ BrowserTestUtils.startLoadingURIString(gBrowser, INSECURE_VIEW_SOURCE_URL);
+ await loaded;
+ info(`${INSECURE_VIEW_SOURCE_URL} finished loading`);
+
+ loaded = promiseIsErrorPage();
+ await waitForAndClickOpenInsecureButton(gBrowser.selectedBrowser);
+ info(`Waiting for normal or error page to load`);
+ const isErrorPage = await loaded;
+
+ ok(!isErrorPage, "We should not land on an error page");
+
+ await Services.perms.removeAll();
+});
diff --git a/toolkit/components/httpsonlyerror/tests/browser/file_errorpage_timeout_server.sjs b/toolkit/components/httpsonlyerror/tests/browser/file_errorpage_timeout_server.sjs
new file mode 100644
index 0000000000..f77d5351b5
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/file_errorpage_timeout_server.sjs
@@ -0,0 +1,15 @@
+// Custom *.sjs file specifically for the needs of Bug 1657348
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ if (request.scheme === "https") {
+ // Simulating a timeout by processing the https request
+ // async and *never* return anything!
+ response.processAsync();
+ return;
+ }
+ // we should never get here; just in case, return something unexpected
+ response.write("do'h");
+}
diff --git a/toolkit/components/httpsonlyerror/tests/browser/file_errorpage_www_suggestion.html b/toolkit/components/httpsonlyerror/tests/browser/file_errorpage_www_suggestion.html
new file mode 100644
index 0000000000..9d5c42ca73
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/file_errorpage_www_suggestion.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1665057 - Add www button on https-only error page</title>
+</head>
+<body>
+ <div>
+ You are now on the secure www. page
+ </div>
+</body>
+</html>
diff --git a/toolkit/components/httpsonlyerror/tests/browser/file_upgrade_insecure_server.sjs b/toolkit/components/httpsonlyerror/tests/browser/file_upgrade_insecure_server.sjs
new file mode 100644
index 0000000000..e05ea7b3a3
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/file_upgrade_insecure_server.sjs
@@ -0,0 +1,108 @@
+// Serverside Javascript for browser_exception.js
+// Bug 1625156 - Error page for HTTPS Only Mode
+
+const expectedQueries = ["content", "img", "iframe", "xhr", "nestedimg"];
+const TOTAL_EXPECTED_REQUESTS = expectedQueries.length;
+
+const CONTENT = path => `
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ </head>
+ <body>
+ <p>Insecure website</p>
+ <script type="application/javascript">
+ var myXHR = new XMLHttpRequest();
+ myXHR.open("GET", "${path}file_upgrade_insecure_server.sjs?xhr");
+ myXHR.send(null);
+ </script>
+ <img src='${path}file_upgrade_insecure_server.sjs?img'></img>
+ <iframe src="${path}file_upgrade_insecure_server.sjs?iframe"></iframe>
+ </body>
+</html>`;
+
+const IFRAME_CONTENT = path => `
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ </head>
+ <body>
+ <p>Nested insecure website</p>
+ <img src='${path}file_upgrade_insecure_server.sjs?nestedimg'></img>
+ </body>
+</html>`;
+
+function handleRequest(request, response) {
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ var queryString = request.queryString;
+
+ // initialize server variables and save the object state
+ // of the initial request, which returns async once the
+ // server has processed all requests.
+ if (queryString.startsWith("queryresult")) {
+ response.processAsync();
+ setState("totaltests", TOTAL_EXPECTED_REQUESTS.toString());
+ setState("receivedQueries", "");
+ setState("rootPath", /=(.+)/.exec(queryString)[1]);
+ setObjectState("queryResult", response);
+ return;
+ }
+
+ // just in case error handling for unexpected queries
+ if (!expectedQueries.includes(queryString)) {
+ response.write("unexpected-response");
+ return;
+ }
+
+ // make sure all the requested queries are indeed http
+ const testResult =
+ queryString + (request.scheme == "http" ? "-ok" : "-error");
+
+ var receivedQueries = getState("receivedQueries");
+
+ // images, scripts, etc. get queried twice, do not
+ // confuse the server by storing the preload as
+ // well as the actual load. If either the preload
+ // or the actual load is not https, then we would
+ // append "-error" in the array and the test would
+ // fail at the end.
+ if (receivedQueries.includes(testResult)) {
+ return;
+ }
+
+ // append the result to the total query string array
+ if (receivedQueries != "") {
+ receivedQueries += ",";
+ }
+ receivedQueries += testResult;
+ setState("receivedQueries", receivedQueries);
+
+ // keep track of how many more requests the server
+ // is expecting
+ var totaltests = parseInt(getState("totaltests"));
+ totaltests -= 1;
+ setState("totaltests", totaltests.toString());
+
+ // Respond with html content
+ if (queryString == "content") {
+ response.write(CONTENT(getState("rootPath")));
+ } else if (queryString == "iframe") {
+ response.write(IFRAME_CONTENT(getState("rootPath")));
+ }
+
+ // if we have received all the requests, we return
+ // the result back.
+ if (totaltests == 0) {
+ getObjectState("queryResult", function (queryResponse) {
+ if (!queryResponse) {
+ return;
+ }
+ var receivedQueries = getState("receivedQueries");
+ queryResponse.write(receivedQueries);
+ queryResponse.finish();
+ });
+ }
+}
diff --git a/toolkit/components/httpsonlyerror/tests/browser/head.js b/toolkit/components/httpsonlyerror/tests/browser/head.js
new file mode 100644
index 0000000000..7efbfbe8b5
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/head.js
@@ -0,0 +1,99 @@
+// Enable HTTPS-Only Mode
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_only_mode", true]],
+ });
+});
+
+// Copied from: https://searchfox.org/mozilla-central/rev/9f074fab9bf905fad62e7cc32faf121195f4ba46/browser/base/content/test/about/head.js
+
+async function injectErrorPageFrame(tab, src, sandboxed) {
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ true,
+ null,
+ true
+ );
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [src, sandboxed],
+ async function (frameSrc, frameSandboxed) {
+ let iframe = content.document.createElement("iframe");
+ iframe.src = frameSrc;
+ if (frameSandboxed) {
+ iframe.setAttribute("sandbox", "allow-scripts");
+ }
+ content.document.body.appendChild(iframe);
+ }
+ );
+
+ await loadedPromise;
+}
+
+async function openErrorPage(src, useFrame, privateWindow, sandboxed) {
+ let gb = gBrowser;
+ if (privateWindow) {
+ gb = privateWindow.gBrowser;
+ }
+ let dummyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "dummy_page.html";
+
+ let tab;
+ if (useFrame) {
+ info("Loading error page in an iframe");
+ tab = await BrowserTestUtils.openNewForegroundTab(gb, dummyPage);
+ await injectErrorPageFrame(tab, src, sandboxed);
+ } else {
+ let ErrorPageLoaded;
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ gb,
+ () => {
+ gb.selectedTab = BrowserTestUtils.addTab(gb, src);
+ let browser = gb.selectedBrowser;
+ ErrorPageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ },
+ false
+ );
+ info("Loading and waiting for the error page");
+ await ErrorPageLoaded;
+ }
+
+ return tab;
+}
+
+/**
+ * On a loaded HTTPS-Only error page, waits until the "Open Insecure"
+ * button gets enabled and then presses it.
+ *
+ * @returns {Promise<void>}
+ */
+function waitForAndClickOpenInsecureButton(browser) {
+ return SpecialPowers.spawn(browser, [], async function () {
+ let openInsecureButton = content.document.getElementById("openInsecure");
+ Assert.notEqual(
+ openInsecureButton,
+ null,
+ "openInsecureButton should exist."
+ );
+ info("Waiting for openInsecureButton to be enabled.");
+ function callback() {
+ if (!openInsecureButton.inert) {
+ info("openInsecureButton was enabled, waiting two frames.");
+ observer.disconnect();
+ content.requestAnimationFrame(() => {
+ content.requestAnimationFrame(() => {
+ info("clicking openInsecureButton.");
+ openInsecureButton.click();
+ });
+ });
+ }
+ }
+ const observer = new content.MutationObserver(callback);
+ observer.observe(openInsecureButton, { attributeFilter: ["inert"] });
+ callback();
+ });
+}