summaryrefslogtreecommitdiffstats
path: root/toolkit/components/httpsonlyerror
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/httpsonlyerror
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/httpsonlyerror')
-rw-r--r--toolkit/components/httpsonlyerror/content/errorpage.html44
-rw-r--r--toolkit/components/httpsonlyerror/content/errorpage.js78
-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.ini10
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/browser_errorpage.js190
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/browser_errorpage_timeout.js48
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/browser_exception.js157
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/file_errorpage_timeout_server.sjs16
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/file_upgrade_insecure_server.sjs108
-rw-r--r--toolkit/components/httpsonlyerror/tests/browser/head.js67
12 files changed, 742 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..186b768041
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/content/errorpage.html
@@ -0,0 +1,44 @@
+<!-- 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'" />
+ <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/warning.svg"/>
+ <title data-l10n-id="about-httpsonly-title-connection-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-connection-not-available"></h1>
+ </div>
+ <p id="insecure-explanation-unavailable" data-l10n-id="about-httpsonly-explanation-unavailable2"></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"></button>
+ </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..a641216c55
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/content/errorpage.js
@@ -0,0 +1,78 @@
+/* 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/frame-script */
+
+"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);
+
+ if (window.top == window) {
+ document
+ .getElementById("goBack")
+ .addEventListener("click", onReturnButtonClick);
+ addAutofocus("#goBack", "beforeend");
+ } else {
+ document.getElementById("goBack").remove();
+ }
+}
+
+/* Button Events */
+
+function onOpenInsecureButtonClick() {
+ RPMSendAsyncMessage("openInsecure", {
+ inFrame: window.top != window,
+ });
+}
+
+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.jsm 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..2eddec4a60
--- /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.ini"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Security")
diff --git a/toolkit/components/httpsonlyerror/tests/browser/browser.ini b/toolkit/components/httpsonlyerror/tests/browser/browser.ini
new file mode 100644
index 0000000000..57611e73e5
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/browser.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files =
+ head.js
+[browser_errorpage.js]
+[browser_exception.js]
+support-files =
+ file_upgrade_insecure_server.sjs
+[browser_errorpage_timeout.js]
+support-files =
+ file_errorpage_timeout_server.sjs
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..d0dd565714
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/browser_errorpage.js
@@ -0,0 +1,190 @@
+/* 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.import(
+ "resource:///modules/sessionstore/TabStateFlusher.jsm"
+);
+
+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.loadURI(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..a3ae4a2bba
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/browser_errorpage_timeout.js
@@ -0,0 +1,48 @@
+/* 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.loadURI(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!"
+ );
+ });
+ });
+});
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..b680c13f42
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/browser_exception.js
@@ -0,0 +1,157 @@
+/* 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
+ );
+
+ // click on exception-button and wait for page to load
+ await SpecialPowers.spawn(browser, [], async function() {
+ let openInsecureButton = content.document.getElementById("openInsecure");
+ ok(openInsecureButton != null, "openInsecureButton should exist.");
+ openInsecureButton.click();
+ });
+
+ 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/file_errorpage_timeout_server.sjs b/toolkit/components/httpsonlyerror/tests/browser/file_errorpage_timeout_server.sjs
new file mode 100644
index 0000000000..0f22b6e6df
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/file_errorpage_timeout_server.sjs
@@ -0,0 +1,16 @@
+// 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_upgrade_insecure_server.sjs b/toolkit/components/httpsonlyerror/tests/browser/file_upgrade_insecure_server.sjs
new file mode 100644
index 0000000000..1f3566c3b8
--- /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..6848401f61
--- /dev/null
+++ b/toolkit/components/httpsonlyerror/tests/browser/head.js
@@ -0,0 +1,67 @@
+/* eslint-env mozilla/frame-script */
+
+// Enable HTTPS-Only Mode
+add_task(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;
+}