diff options
Diffstat (limited to 'toolkit/components/httpsonlyerror')
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(); + }); +} |