diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /netwerk/test/browser | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
122 files changed, 5942 insertions, 0 deletions
diff --git a/netwerk/test/browser/103_preload.html b/netwerk/test/browser/103_preload.html new file mode 100644 index 0000000000..9583815cfb --- /dev/null +++ b/netwerk/test/browser/103_preload.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +<img src="https://example.com/browser/netwerk/test/browser/square.png" width="100px"> +</body> +</html> diff --git a/netwerk/test/browser/103_preload.html^headers^ b/netwerk/test/browser/103_preload.html^headers^ new file mode 100644 index 0000000000..9e23c73b7f --- /dev/null +++ b/netwerk/test/browser/103_preload.html^headers^ @@ -0,0 +1 @@ +Cache-Control: no-cache diff --git a/netwerk/test/browser/103_preload.html^informationalResponse^ b/netwerk/test/browser/103_preload.html^informationalResponse^ new file mode 100644 index 0000000000..b95a96e74b --- /dev/null +++ b/netwerk/test/browser/103_preload.html^informationalResponse^ @@ -0,0 +1,2 @@ +HTTP 103 Too Early +Link: <https://example.com/browser/netwerk/test/browser/square.png>; rel=preload; as=image diff --git a/netwerk/test/browser/103_preload_anchor.html b/netwerk/test/browser/103_preload_anchor.html new file mode 100644 index 0000000000..c12fe92072 --- /dev/null +++ b/netwerk/test/browser/103_preload_anchor.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +<img src="https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs?f5a05cb8-43e6-4868-bc0f-ca453ef87826" width="100px"> +</body> +</html> diff --git a/netwerk/test/browser/103_preload_anchor.html^headers^ b/netwerk/test/browser/103_preload_anchor.html^headers^ new file mode 100644 index 0000000000..9e23c73b7f --- /dev/null +++ b/netwerk/test/browser/103_preload_anchor.html^headers^ @@ -0,0 +1 @@ +Cache-Control: no-cache diff --git a/netwerk/test/browser/103_preload_anchor.html^informationalResponse^ b/netwerk/test/browser/103_preload_anchor.html^informationalResponse^ new file mode 100644 index 0000000000..1099062e15 --- /dev/null +++ b/netwerk/test/browser/103_preload_anchor.html^informationalResponse^ @@ -0,0 +1,2 @@ +HTTP 103 Early Hints +Link: <netwerk/test/browser/early_hint_pixel.sjs?f5a05cb8-43e6-4868-bc0f-ca453ef87826>; rel=preload; as=image; anchor="/browser/" diff --git a/netwerk/test/browser/103_preload_and_404.html b/netwerk/test/browser/103_preload_and_404.html new file mode 100644 index 0000000000..f09f5cb085 --- /dev/null +++ b/netwerk/test/browser/103_preload_and_404.html @@ -0,0 +1,6 @@ +<html> + <head><title>404 Not Found</title></head> + <body> + <h1>404 Not Found</h1> + </body> +</html> diff --git a/netwerk/test/browser/103_preload_and_404.html^headers^ b/netwerk/test/browser/103_preload_and_404.html^headers^ new file mode 100644 index 0000000000..937e38c6c4 --- /dev/null +++ b/netwerk/test/browser/103_preload_and_404.html^headers^ @@ -0,0 +1 @@ +HTTP 404 Not Found diff --git a/netwerk/test/browser/103_preload_and_404.html^informationalResponse^ b/netwerk/test/browser/103_preload_and_404.html^informationalResponse^ new file mode 100644 index 0000000000..78cb7efea4 --- /dev/null +++ b/netwerk/test/browser/103_preload_and_404.html^informationalResponse^ @@ -0,0 +1,2 @@ +HTTP 103 Early Hints +Link: <https://example.com/browser/netwerk/test/browser/square.png>; rel=preload; as=image diff --git a/netwerk/test/browser/103_preload_csp_imgsrc_none.html b/netwerk/test/browser/103_preload_csp_imgsrc_none.html new file mode 100644 index 0000000000..367e80a6b3 --- /dev/null +++ b/netwerk/test/browser/103_preload_csp_imgsrc_none.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +<img src="https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs?1ac2a5e1-90c7-4171-b0f0-676f7d899af3" width="100px"> +</body> +</html> diff --git a/netwerk/test/browser/103_preload_csp_imgsrc_none.html^headers^ b/netwerk/test/browser/103_preload_csp_imgsrc_none.html^headers^ new file mode 100644 index 0000000000..b4dedd0812 --- /dev/null +++ b/netwerk/test/browser/103_preload_csp_imgsrc_none.html^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-cache +Content-Security-Policy: img-src 'none' diff --git a/netwerk/test/browser/103_preload_csp_imgsrc_none.html^informationalResponse^ b/netwerk/test/browser/103_preload_csp_imgsrc_none.html^informationalResponse^ new file mode 100644 index 0000000000..d82224fd07 --- /dev/null +++ b/netwerk/test/browser/103_preload_csp_imgsrc_none.html^informationalResponse^ @@ -0,0 +1,2 @@ +HTTP 103 Too Early +Link: <https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs?1ac2a5e1-90c7-4171-b0f0-676f7d899af3>; rel=preload; as=image diff --git a/netwerk/test/browser/103_preload_iframe.html b/netwerk/test/browser/103_preload_iframe.html new file mode 100644 index 0000000000..815a14220f --- /dev/null +++ b/netwerk/test/browser/103_preload_iframe.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +<iframe src="/browser/netwerk/test/browser/early_hint_main_html.sjs?early_hint_pixel.sjs=5ecccd01-dd3f-4bbd-bd3e-0491d7dd78a1" width="100px"> +</body> +</html> diff --git a/netwerk/test/browser/103_preload_iframe.html^headers^ b/netwerk/test/browser/103_preload_iframe.html^headers^ new file mode 100644 index 0000000000..9e23c73b7f --- /dev/null +++ b/netwerk/test/browser/103_preload_iframe.html^headers^ @@ -0,0 +1 @@ +Cache-Control: no-cache diff --git a/netwerk/test/browser/auth_post.sjs b/netwerk/test/browser/auth_post.sjs new file mode 100644 index 0000000000..8c3e723558 --- /dev/null +++ b/netwerk/test/browser/auth_post.sjs @@ -0,0 +1,37 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function readStream(inputStream) { + let available = 0; + let result = []; + while ((available = inputStream.available()) > 0) { + result.push(inputStream.readBytes(available)); + } + + return result.join(""); +} + +function handleRequest(request, response) { + if (request.method != "POST") { + response.setStatusLine(request.httpVersion, 405, "Method Not Allowed"); + return; + } + + if (request.hasHeader("Authorization")) { + let data = ""; + try { + data = readStream(new BinaryInputStream(request.bodyInputStream)); + } catch (e) { + data = `${e}`; + } + response.bodyOutputStream.write(data, data.length); + return; + } + + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `basic realm="test"`, true); +} diff --git a/netwerk/test/browser/browser.toml b/netwerk/test/browser/browser.toml new file mode 100644 index 0000000000..9a470d8c59 --- /dev/null +++ b/netwerk/test/browser/browser.toml @@ -0,0 +1,188 @@ +[DEFAULT] +support-files = [ + "dummy.html", + "ioactivity.html", + "redirect.sjs", + "auth_post.sjs", + "early_hint_main_html.sjs", + "early_hint_pixel_count.sjs", + "early_hint_redirect.sjs", + "early_hint_redirect_html.sjs", + "early_hint_pixel.sjs", + "early_hint_error.sjs", + "early_hint_asset.sjs", + "early_hint_asset_html.sjs", + "early_hint_csp_options_html.sjs", + "early_hint_preconnect_html.sjs", + "post.html", + "res.css", + "res.css^headers^", + "res.csv", + "res.csv^headers^", + "res_206.html", + "res_206.html^headers^", + "res_nosniff.html", + "res_nosniff.html^headers^", + "res_img.png", + "res_nosniff2.html", + "res_nosniff2.html^headers^", + "res_not_ok.html", + "res_not_ok.html^headers^", + "res.unknown", + "res_img_unknown.png", + "res.mp3", + "res_invalid_partial.mp3", + "res_invalid_partial.mp3^headers^", + "res_206.mp3", + "res_206.mp3^headers^", + "res_not_200or206.mp3", + "res_not_200or206.mp3^headers^", + "res_img_for_unknown_decoder", + "res_img_for_unknown_decoder^headers^", + "res_object.html", + "res_sub_document.html", + "res_empty.zip", + "res_http_index_format", + "res_http_index_format^headers^", + "square.png", + "103_preload.html", + "103_preload.html^informationalResponse^", + "103_preload.html^headers^", + "no_103_preload.html", + "no_103_preload.html^headers^", + "103_preload_anchor.html^informationalResponse^", + "103_preload_anchor.html^headers^", + "103_preload_anchor.html", + "103_preload_and_404.html^informationalResponse^", + "103_preload_and_404.html^headers^", + "103_preload_and_404.html", + "103_preload_iframe.html", + "103_preload_iframe.html^headers^", + "103_preload_csp_imgsrc_none.html", + "103_preload_csp_imgsrc_none.html^headers^", + "103_preload_csp_imgsrc_none.html^informationalResponse^", + "cookie_filtering_resource.sjs", + "cookie_filtering_secure_resource_com.html", + "cookie_filtering_secure_resource_com.html^headers^", + "cookie_filtering_secure_resource_org.html", + "cookie_filtering_secure_resource_org.html^headers^", + "cookie_filtering_square.png", + "cookie_filtering_square.png^headers^", + "x_frame_options.html", + "x_frame_options.html^headers^", + "test_1629307.html", + "file_link_header.sjs", +] + +["browser_103_assets.js"] + +["browser_103_assets_extension.js"] + +["browser_103_cleanup.js"] + +["browser_103_csp.js"] +support-files = ["early_hint_preload_test_helper.sys.mjs",] + +["browser_103_csp_images.js"] +support-files = ["early_hint_preload_test_helper.sys.mjs",] + +["browser_103_csp_styles.js"] +support-files = ["early_hint_preload_test_helper.sys.mjs",] + +["browser_103_error.js"] +support-files = ["early_hint_preload_test_helper.sys.mjs",] + +["browser_103_no_cancel_on_error.js"] +support-files = ["early_hint_preload_test_helper.sys.mjs",] + +["browser_103_preconnect.js"] + +["browser_103_preload.js"] +support-files = ["early_hint_preload_test_helper.sys.mjs",] + +["browser_103_preload_2.js"] +support-files = ["early_hint_preload_test_helper.sys.mjs",] + +["browser_103_private_window.js"] + +["browser_103_redirect.js"] +support-files = ["early_hint_preload_test_helper.sys.mjs",] + +["browser_103_redirect_from_server.js"] + +["browser_103_referrer_policy.js"] +support-files = [ + "early_hint_referrer_policy_html.sjs", + "early_hint_preload_test_helper.sys.mjs", +] + +["browser_103_telemetry.js"] + +["browser_103_user_load.js"] +support-files = ["early_hint_preload_test_helper.sys.mjs",] + +["browser_NetUtil.js"] + +["browser_about_cache.js"] + +["browser_backgroundtask_purgeHTTPCache.js"] +skip-if = [ + "os == 'android'", # MultiInstanceLock doesn't work on Android + "os == 'mac'", # intermittent TV timeouts on Mac +] + +["browser_bug1535877.js"] + +["browser_bug1629307.js"] +fail-if = ["a11y_checks"] # Bug 1854523 clicked button may not be focusable + +["browser_child_resource.js"] +run-if = ["crashreporter"] +skip-if = ["os == 'win'"] # Bug 1775761 + +["browser_cookie_filtering_basic.js"] + +["browser_cookie_filtering_cross_origin.js"] + +["browser_cookie_filtering_insecure.js"] + +["browser_cookie_filtering_oa.js"] + +["browser_cookie_filtering_subdomain.js"] + +["browser_cookie_sync_across_tabs.js"] + +["browser_fetch_lnk.js"] +run-if = ["os == 'win'"] +support-files = ["file_lnk.lnk",] + +["browser_http_index_format.js"] + +["browser_nsIFormPOSTActionChannel.js"] +skip-if = ["true"] # protocol handler and channel does not work in content process + +["browser_post_auth.js"] +skip-if = ["socketprocess_networking"] # Bug 1772209 + +["browser_post_file.js"] + +["browser_purgeCache_idle_daily.js"] + +["browser_resource_navigation.js"] + +["browser_speculative_connection_link_header.js"] + +["browser_test_data_channel_observer.js"] + +["browser_test_favicon.js"] +skip-if = ["verify && (os == 'linux' || os == 'mac')"] +support-files = [ + "damonbowling.jpg", + "damonbowling.jpg^headers^", + "file_favicon.html", +] + +["browser_test_io_activity.js"] +skip-if = ["socketprocess_networking"] + +["browser_test_offline_tab.js"] diff --git a/netwerk/test/browser/browser_103_assets.js b/netwerk/test/browser/browser_103_assets.js new file mode 100644 index 0000000000..4e32c8bf7c --- /dev/null +++ b/netwerk/test/browser/browser_103_assets.js @@ -0,0 +1,169 @@ +/* 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/. */ + +// On debug osx test machine, verify chaos mode takes slightly too long +requestLongerTimeout(2); + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +const { request_count_checking } = ChromeUtils.importESModule( + "resource://testing-common/early_hint_preload_test_helper.sys.mjs" +); + +// - testName is just there to be printed during Asserts when failing +// - asset is the asset type, see early_hint_asset_html.sjs for possible values +// for the asset type fetch see test_hint_fetch due to timing issues +// - variant: +// - "normal": no early hints, expects one normal request expected +// - "hinted": early hints sent, expects one hinted request +// - "reload": early hints sent, resources non-cacheable, two early-hint requests expected +// - "cached": same as reload, but resources are cacheable, so only one hinted network request expected +async function test_hint_asset(testName, asset, variant) { + // reset the count + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "http://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + let requestUrl = `https://example.com/browser/netwerk/test/browser/early_hint_asset_html.sjs?as=${asset}&hinted=${ + variant !== "normal" ? "1" : "0" + }&cached=${variant === "cached" ? "1" : "0"}`; + + let numConnectBackRemaining = 0; + if (variant === "hinted") { + numConnectBackRemaining = 1; + } else if (variant === "reload" || variant === "cached") { + numConnectBackRemaining = 2; + } + + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == "earlyhints-connectback") { + numConnectBackRemaining -= 1; + } + }, + }; + Services.obs.addObserver(observer, "earlyhints-connectback"); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: requestUrl, + waitForLoad: true, + }, + async function (browser) { + if (asset === "fetch") { + // wait until the fetch is complete + await TestUtils.waitForCondition(_ => { + return SpecialPowers.spawn(browser, [], _ => { + return ( + content.document.getElementsByTagName("h2")[0] != undefined && + content.document.getElementsByTagName("h2")[0].textContent !== + "Fetching..." // default text set by early_hint_asset_html.sjs + ); + }); + }); + } + + // reload + if (variant === "reload" || variant === "cached") { + await BrowserTestUtils.reloadTab(gBrowser.selectedTab); + } + + if (asset === "fetch") { + // wait until the fetch is complete + await TestUtils.waitForCondition(_ => { + return SpecialPowers.spawn(browser, [], _ => { + return ( + content.document.getElementsByTagName("h2")[0] != undefined && + content.document.getElementsByTagName("h2")[0].textContent !== + "Fetching..." // default text set by early_hint_asset_html.sjs + ); + }); + }); + } + } + ); + Services.obs.removeObserver(observer, "earlyhints-connectback"); + + let gotRequestCount = await fetch( + "http://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs" + ).then(response => response.json()); + Assert.equal( + numConnectBackRemaining, + 0, + `${testName} (${asset}) no remaining connect back expected` + ); + + let expectedRequestCount; + if (variant === "normal") { + expectedRequestCount = { hinted: 0, normal: 1 }; + } else if (variant === "hinted") { + expectedRequestCount = { hinted: 1, normal: 0 }; + } else if (variant === "reload") { + expectedRequestCount = { hinted: 2, normal: 0 }; + } else if (variant === "cached") { + expectedRequestCount = { hinted: 1, normal: 0 }; + } + + await request_count_checking( + `${testName} (${asset})`, + gotRequestCount, + expectedRequestCount + ); + if (variant === "cached") { + Services.cache2.clear(); + } +} + +// preload image +add_task(async function test_103_asset_image() { + await test_hint_asset("test_103_asset_normal", "image", "normal"); + await test_hint_asset("test_103_asset_hinted", "image", "hinted"); + await test_hint_asset("test_103_asset_reload", "image", "reload"); + // TODO(Bug 1815884): await test_hint_asset("test_103_asset_cached", "image", "cached"); +}); + +// preload css +add_task(async function test_103_asset_style() { + await test_hint_asset("test_103_asset_normal", "style", "normal"); + await test_hint_asset("test_103_asset_hinted", "style", "hinted"); + await test_hint_asset("test_103_asset_reload", "style", "reload"); + // TODO(Bug 1815884): await test_hint_asset("test_103_asset_cached", "style", "cached"); +}); + +// preload javascript +add_task(async function test_103_asset_javascript() { + await test_hint_asset("test_103_asset_normal", "script", "normal"); + await test_hint_asset("test_103_asset_hinted", "script", "hinted"); + await test_hint_asset("test_103_asset_reload", "script", "reload"); + await test_hint_asset("test_103_asset_cached", "script", "cached"); +}); + +// preload javascript module +add_task(async function test_103_asset_module() { + await test_hint_asset("test_103_asset_normal", "module", "normal"); + await test_hint_asset("test_103_asset_hinted", "module", "hinted"); + await test_hint_asset("test_103_asset_reload", "module", "reload"); + await test_hint_asset("test_103_asset_cached", "module", "cached"); +}); + +// preload font +add_task(async function test_103_asset_font() { + await test_hint_asset("test_103_asset_normal", "font", "normal"); + await test_hint_asset("test_103_asset_hinted", "font", "hinted"); + await test_hint_asset("test_103_asset_reload", "font", "reload"); + await test_hint_asset("test_103_asset_cached", "font", "cached"); +}); + +// preload fetch +add_task(async function test_103_asset_fetch() { + await test_hint_asset("test_103_asset_normal", "fetch", "normal"); + await test_hint_asset("test_103_asset_hinted", "fetch", "hinted"); + await test_hint_asset("test_103_asset_reload", "fetch", "reload"); + await test_hint_asset("test_103_asset_cached", "fetch", "cached"); +}); diff --git a/netwerk/test/browser/browser_103_assets_extension.js b/netwerk/test/browser/browser_103_assets_extension.js new file mode 100644 index 0000000000..dbd1061380 --- /dev/null +++ b/netwerk/test/browser/browser_103_assets_extension.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/. */ + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +registerCleanupFunction(function () { + Services.prefs.clearUserPref("network.early-hints.enabled"); +}); + +const { request_count_checking } = ChromeUtils.importESModule( + "resource://testing-common/early_hint_preload_test_helper.sys.mjs" +); + +// Test steps: +// 1. Install an extension to observe the tabId. +// 2. Load early_hint_asset_html.sjs?as=image, so we should see a hinted +// request to early_hint_asset.sjs?as=image. +// 3. Check if the hinted request has the same tabId as +// early_hint_asset_html.sys. +add_task(async function test_103_asset_with_extension() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "webRequest", + "webRequestBlocking", + "https://example.com/*", + ], + }, + background() { + let { browser } = this; + browser.webRequest.onBeforeRequest.addListener( + details => { + browser.test.sendMessage("request", { + url: details.url, + tabId: details.tabId, + }); + return undefined; + }, + { urls: ["https://example.com/*"] }, + ["blocking"] + ); + }, + }); + + await extension.startup(); + + // reset the count + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + let baseUrl = "https://example.com/browser/netwerk/test/browser/"; + let requestUrl = `${baseUrl}early_hint_asset_html.sjs?as=image&hinted=1`; + let assetUrl = `${baseUrl}early_hint_asset.sjs?as=image`; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: requestUrl, + waitForLoad: true, + }, + async function () {} + ); + + const result1 = await extension.awaitMessage("request"); + Assert.equal(result1.url, requestUrl); + let tabId = result1.tabId; + + const result2 = await extension.awaitMessage("request"); + Assert.equal(result2.url, assetUrl); + Assert.equal(result2.tabId, tabId); + + await extension.unload(); +}); diff --git a/netwerk/test/browser/browser_103_cleanup.js b/netwerk/test/browser/browser_103_cleanup.js new file mode 100644 index 0000000000..c823f5f01a --- /dev/null +++ b/netwerk/test/browser/browser_103_cleanup.js @@ -0,0 +1,47 @@ +/* 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"; + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +add_task(async function test_103_cancel_parent_connect() { + Services.prefs.setIntPref("network.early-hints.parent-connect-timeout", 1); + + let callback; + let promise = new Promise(resolve => { + callback = resolve; + }); + let observed_cancel_reason = ""; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + aSubject = aSubject.QueryInterface(Ci.nsIRequest); + if ( + aTopic == "http-on-stop-request" && + aSubject.name == + "https://example.com/browser/netwerk/test/browser/square.png" + ) { + observed_cancel_reason = aSubject.canceledReason; + Services.obs.removeObserver(observer, "http-on-stop-request"); + callback(); + } + }, + }; + Services.obs.addObserver(observer, "http-on-stop-request"); + + // test that no crash or memory leak happens when cancelling before + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "https://example.com/browser/netwerk/test/browser/103_preload.html", + waitForLoad: true, + }, + async function () {} + ); + await promise; + Assert.equal(observed_cancel_reason, "parent-connect-timeout"); + + Services.prefs.clearUserPref("network.early-hints.parent-connect-timeout"); +}); diff --git a/netwerk/test/browser/browser_103_csp.js b/netwerk/test/browser/browser_103_csp.js new file mode 100644 index 0000000000..1786bac454 --- /dev/null +++ b/netwerk/test/browser/browser_103_csp.js @@ -0,0 +1,86 @@ +/* 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"; + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +const { test_preload_hint_and_request } = ChromeUtils.importESModule( + "resource://testing-common/early_hint_preload_test_helper.sys.mjs" +); + +add_task(async function test_preload_images_csp_in_early_hints_response() { + let tests = [ + { + input: { + test_name: "image - no csp", + resource_type: "image", + csp: "", + csp_in_early_hint: "", + host: "", + hinted: true, + }, + expected: { hinted: 1, normal: 0 }, + }, + { + input: { + test_name: "image img-src 'self';", + resource_type: "image", + csp: "", + csp_in_early_hint: "img-src 'self';", + host: "", + hinted: true, + }, + expected: { hinted: 1, normal: 0 }, + }, + { + input: { + test_name: "image img-src 'self'; same host provided", + resource_type: "image", + csp: "", + csp_in_early_hint: "img-src 'self';", + host: "https://example.com/browser/netwerk/test/browser/", + hinted: true, + }, + expected: { hinted: 1, normal: 0 }, + }, + { + input: { + test_name: "image img-src 'self'; other host provided", + resource_type: "image", + csp: "", + csp_in_early_hint: "img-src 'self';", + host: "https://example.org/browser/netwerk/test/browser/", + hinted: true, + }, + expected: { hinted: 0, normal: 1 }, + }, + { + input: { + test_name: "image img-src 'none';", + resource_type: "image", + csp: "", + csp_in_early_hint: "img-src 'none';", + host: "", + hinted: true, + }, + expected: { hinted: 0, normal: 1 }, + }, + { + input: { + test_name: "image img-src 'none'; same host provided", + resource_type: "image", + csp: "", + csp_in_early_hint: "img-src 'none';", + host: "https://example.com/browser/netwerk/test/browser/", + hinted: true, + }, + expected: { hinted: 0, normal: 1 }, + }, + ]; + + for (let test of tests) { + await test_preload_hint_and_request(test.input, test.expected); + } +}); diff --git a/netwerk/test/browser/browser_103_csp_images.js b/netwerk/test/browser/browser_103_csp_images.js new file mode 100644 index 0000000000..c089f29898 --- /dev/null +++ b/netwerk/test/browser/browser_103_csp_images.js @@ -0,0 +1,170 @@ +/* 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"; + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +// This verifies hints, requests server-side and client-side that the image actually loaded +async function test_image_preload_hint_request_loaded( + input, + expected_results, + image_should_load +) { + // reset the count + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + let requestUrl = `https://example.com/browser/netwerk/test/browser/early_hint_csp_options_html.sjs?as=${ + input.resource_type + }&hinted=${input.hinted ? "1" : "0"}${input.csp ? "&csp=" + input.csp : ""}${ + input.csp_in_early_hint + ? "&csp_in_early_hint=" + input.csp_in_early_hint + : "" + }${input.host ? "&host=" + input.host : ""}`; + + console.log("requestUrl: " + requestUrl); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: requestUrl, + waitForLoad: true, + }, + async function (browser) { + let imageLoaded = await ContentTask.spawn(browser, [], function () { + let image = content.document.getElementById("test_image"); + return image && image.complete && image.naturalHeight !== 0; + }); + await Assert.ok( + image_should_load == imageLoaded, + "test_image_preload_hint_request_loaded: the image can be loaded as expected " + + requestUrl + ); + } + ); + + let gotRequestCount = await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs" + ).then(response => response.json()); + + await Assert.deepEqual(gotRequestCount, expected_results, input.test_name); + + Services.cache2.clear(); +} + +// These tests verify whether or not the image actually loaded in the document +add_task(async function test_images_loaded_with_csp() { + let tests = [ + { + input: { + test_name: "image loaded - no csp", + resource_type: "image", + csp: "", + csp_in_early_hint: "", + host: "", + hinted: true, + }, + expected: { hinted: 1, normal: 0 }, + image_should_load: true, + }, + { + input: { + test_name: "image loaded - img-src none", + resource_type: "image", + csp: "img-src 'none';", + csp_in_early_hint: "", + host: "", + hinted: true, + }, + expected: { hinted: 1, normal: 0 }, + image_should_load: false, + }, + { + input: { + test_name: "image loaded - img-src none in EH response", + resource_type: "image", + csp: "", + csp_in_early_hint: "img-src 'none';", + host: "", + hinted: true, + }, + expected: { hinted: 0, normal: 1 }, + image_should_load: true, + }, + { + input: { + test_name: "image loaded - img-src none in both headers", + resource_type: "image", + csp: "img-src 'none';", + csp_in_early_hint: "img-src 'none';", + host: "", + hinted: true, + }, + expected: { hinted: 0, normal: 0 }, + image_should_load: false, + }, + { + input: { + test_name: "image loaded - img-src self", + resource_type: "image", + csp: "img-src 'self';", + csp_in_early_hint: "", + host: "", + hinted: true, + }, + expected: { hinted: 1, normal: 0 }, + image_should_load: true, + }, + { + input: { + test_name: "image loaded - img-src self in EH response", + resource_type: "image", + csp: "", + csp_in_early_hint: "img-src 'self';", + host: "", + hinted: true, + }, + expected: { hinted: 1, normal: 0 }, + image_should_load: true, + }, + { + input: { + test_name: "image loaded - conflicting csp, early hint skipped", + resource_type: "image", + csp: "img-src 'self';", + csp_in_early_hint: "img-src 'none';", + host: "", + hinted: true, + }, + expected: { hinted: 0, normal: 1 }, + image_should_load: true, + }, + { + input: { + test_name: + "image loaded - conflicting csp, resource not loaded in document", + resource_type: "image", + csp: "img-src 'none';", + csp_in_early_hint: "img-src 'self';", + host: "", + hinted: true, + }, + expected: { hinted: 1, normal: 0 }, + image_should_load: false, + }, + ]; + + for (let test of tests) { + await test_image_preload_hint_request_loaded( + test.input, + test.expected, + test.image_should_load + ); + } +}); diff --git a/netwerk/test/browser/browser_103_csp_styles.js b/netwerk/test/browser/browser_103_csp_styles.js new file mode 100644 index 0000000000..59c2fc14be --- /dev/null +++ b/netwerk/test/browser/browser_103_csp_styles.js @@ -0,0 +1,86 @@ +/* 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"; + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +const { test_preload_hint_and_request } = ChromeUtils.importESModule( + "resource://testing-common/early_hint_preload_test_helper.sys.mjs" +); + +add_task(async function test_preload_styles_csp_in_response() { + let tests = [ + { + input: { + test_name: "style - no csp", + resource_type: "style", + csp: "", + csp_in_early_hint: "", + host: "", + hinted: true, + }, + expected: { hinted: 1, normal: 0 }, + }, + { + input: { + test_name: "style style-src 'self';", + resource_type: "style", + csp: "", + csp_in_early_hint: "style-src 'self';", + host: "", + hinted: true, + }, + expected: { hinted: 1, normal: 0 }, + }, + { + input: { + test_name: "style style-src self; same host provided", + resource_type: "style", + csp: "", + csp_in_early_hint: "style-src 'self';", + host: "https://example.com/browser/netwerk/test/browser/", + hinted: true, + }, + expected: { hinted: 1, normal: 0 }, + }, + { + input: { + test_name: "style style-src 'self'; other host provided", + resource_type: "style", + csp: "", + csp_in_early_hint: "style-src 'self';", + host: "https://example.org/browser/netwerk/test/browser/", + hinted: true, + }, + expected: { hinted: 0, normal: 1 }, + }, + { + input: { + test_name: "style style-src 'none';", + resource_type: "style", + csp: "", + csp_in_early_hint: "style-src 'none';", + host: "", + hinted: true, + }, + expected: { hinted: 0, normal: 1 }, + }, + { + input: { + test_name: "style style-src 'none'; other host provided", + resource_type: "style", + csp: "", + csp_in_early_hint: "style-src 'none';", + host: "https://example.org/browser/netwerk/test/browser/", + hinted: true, + }, + expected: { hinted: 0, normal: 1 }, + }, + ]; + + for (let test of tests) { + await test_preload_hint_and_request(test.input, test.expected); + } +}); diff --git a/netwerk/test/browser/browser_103_error.js b/netwerk/test/browser/browser_103_error.js new file mode 100644 index 0000000000..a7a447aa7e --- /dev/null +++ b/netwerk/test/browser/browser_103_error.js @@ -0,0 +1,121 @@ +/* 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"; + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +const { test_hint_preload } = ChromeUtils.importESModule( + "resource://testing-common/early_hint_preload_test_helper.sys.mjs" +); + +// 400 Bad Request +add_task(async function test_103_error_400() { + await test_hint_preload( + "test_103_error_400", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_error.sjs?400", + { hinted: 1, normal: 0 } + ); +}); + +// 401 Unauthorized +add_task(async function test_103_error_401() { + await test_hint_preload( + "test_103_error_401", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_error.sjs?401", + { hinted: 1, normal: 0 } + ); +}); + +// 403 Forbidden +add_task(async function test_103_error_403() { + await test_hint_preload( + "test_103_error_403", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_error.sjs?403", + { hinted: 1, normal: 0 } + ); +}); + +// 404 Not Found +add_task(async function test_103_error_404() { + await test_hint_preload( + "test_103_error_404", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_error.sjs?404", + { hinted: 1, normal: 0 } + ); +}); + +// 408 Request Timeout +add_task(async function test_103_error_408() { + await test_hint_preload( + "test_103_error_408", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_error.sjs?408", + { hinted: 1, normal: 0 } + ); +}); + +// 410 Gone +add_task(async function test_103_error_410() { + await test_hint_preload( + "test_103_error_410", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_error.sjs?410", + { hinted: 1, normal: 0 } + ); +}); + +// 429 Too Many Requests +add_task(async function test_103_error_429() { + await test_hint_preload( + "test_103_error_429", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_error.sjs?429", + { hinted: 1, normal: 0 } + ); +}); + +// 500 Internal Server Error +add_task(async function test_103_error_500() { + await test_hint_preload( + "test_103_error_500", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_error.sjs?500", + { hinted: 1, normal: 0 } + ); +}); + +// 502 Bad Gateway +add_task(async function test_103_error_502() { + await test_hint_preload( + "test_103_error_502", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_error.sjs?502", + { hinted: 1, normal: 0 } + ); +}); + +// 503 Service Unavailable +add_task(async function test_103_error_503() { + await test_hint_preload( + "test_103_error_503", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_error.sjs?503", + { hinted: 1, normal: 0 } + ); +}); + +// 504 Gateway Timeout +add_task(async function test_103_error_504() { + await test_hint_preload( + "test_103_error_504", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_error.sjs?504", + { hinted: 1, normal: 0 } + ); +}); diff --git a/netwerk/test/browser/browser_103_no_cancel_on_error.js b/netwerk/test/browser/browser_103_no_cancel_on_error.js new file mode 100644 index 0000000000..2420441585 --- /dev/null +++ b/netwerk/test/browser/browser_103_no_cancel_on_error.js @@ -0,0 +1,67 @@ +/* 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/. */ + +/* 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/. */ + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +const { request_count_checking } = ChromeUtils.importESModule( + "resource://testing-common/early_hint_preload_test_helper.sys.mjs" +); + +// - httpCode is the response code we're testing for. This file mostly covers 400 and 500 responses +async function test_hint_completion_on_error(httpCode) { + // reset the count + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + let requestUrl = `https://example.com/browser/netwerk/test/browser/early_hint_asset_html.sjs?hinted=1&as=image&code=${httpCode}`; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: requestUrl, + waitForLoad: true, + }, + async function () {} + ); + + let gotRequestCount = await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs" + ).then(response => response.json()); + + await request_count_checking(`test_103_error_${httpCode}`, gotRequestCount, { + hinted: 1, + normal: 0, + }); +} + +// 400 Bad Request +add_task(async function test_complete_103_on_400() { + await test_hint_completion_on_error(400); +}); +add_task(async function test_complete_103_on_401() { + await test_hint_completion_on_error(401); +}); +add_task(async function test_complete_103_on_402() { + await test_hint_completion_on_error(402); +}); +add_task(async function test_complete_103_on_403() { + await test_hint_completion_on_error(403); +}); +add_task(async function test_complete_103_on_500() { + await test_hint_completion_on_error(500); +}); +add_task(async function test_complete_103_on_501() { + await test_hint_completion_on_error(501); +}); +add_task(async function test_complete_103_on_502() { + await test_hint_completion_on_error(502); +}); diff --git a/netwerk/test/browser/browser_103_preconnect.js b/netwerk/test/browser/browser_103_preconnect.js new file mode 100644 index 0000000000..dcdcc1b138 --- /dev/null +++ b/netwerk/test/browser/browser_103_preconnect.js @@ -0,0 +1,71 @@ +/* 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/. */ + +Services.prefs.setBoolPref("network.early-hints.enabled", true); +Services.prefs.setBoolPref("network.early-hints.preconnect.enabled", true); +Services.prefs.setBoolPref("network.http.debug-observations", true); +Services.prefs.setIntPref("network.early-hints.preconnect.max_connections", 10); + +registerCleanupFunction(function () { + Services.prefs.clearUserPref("network.early-hints.enabled"); + Services.prefs.clearUserPref("network.early-hints.preconnect.enabled"); + Services.prefs.clearUserPref("network.http.debug-observations"); + Services.prefs.clearUserPref( + "network.early-hints.preconnect.max_connections" + ); +}); + +// Test steps: +// 1. Load early_hint_preconnect_html.sjs +// 2. In early_hint_preconnect_html.sjs, a 103 response with +// "rel=preconnect" is returned. +// 3. We use "speculative-connect-request" topic to observe whether the +// speculative connection is attempted. +// 4. Finally, we check if the observed URL is the same as the expected. +async function test_hint_preconnect(href, crossOrigin) { + let requestUrl = `https://example.com/browser/netwerk/test/browser/early_hint_preconnect_html.sjs?href=${href}&crossOrigin=${crossOrigin}`; + + let observed = ""; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == "speculative-connect-request") { + Services.obs.removeObserver(observer, "speculative-connect-request"); + observed = aData; + } + }, + }; + Services.obs.addObserver(observer, "speculative-connect-request"); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: requestUrl, + waitForLoad: true, + }, + async function () {} + ); + + // Extracting "localhost:443" + let hostPortRegex = /\[.*\](.*?)\^/; + let hostPortMatch = hostPortRegex.exec(observed); + let hostPort = hostPortMatch ? hostPortMatch[1] : ""; + // Extracting "%28https%2Cexample.com%29" + let partitionKeyRegex = /\^partitionKey=(.*)$/; + let partitionKeyMatch = partitionKeyRegex.exec(observed); + let partitionKey = partitionKeyMatch ? partitionKeyMatch[1] : ""; + // See nsHttpConnectionInfo::BuildHashKey, the second character is A if this + // is an anonymous connection. + let anonymousFlag = observed[2]; + + Assert.equal(anonymousFlag, crossOrigin === "use-credentials" ? "." : "A"); + Assert.equal(hostPort, "localhost:443"); + Assert.equal(partitionKey, "%28https%2Cexample.com%29"); +} + +add_task(async function test_103_preconnect() { + await test_hint_preconnect("https://localhost", "use-credentials"); + await test_hint_preconnect("https://localhost", ""); + await test_hint_preconnect("https://localhost", "anonymous"); +}); diff --git a/netwerk/test/browser/browser_103_preload.js b/netwerk/test/browser/browser_103_preload.js new file mode 100644 index 0000000000..977ae83d68 --- /dev/null +++ b/netwerk/test/browser/browser_103_preload.js @@ -0,0 +1,138 @@ +/* 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"; + +Services.prefs.setBoolPref("network.early-hints.enabled", true); +// Disable mixed-content upgrading as this test is expecting HTTP image loads +Services.prefs.setBoolPref( + "security.mixed_content.upgrade_display_content", + false +); + +const { request_count_checking, test_preload_url, test_hint_preload } = + ChromeUtils.importESModule( + "resource://testing-common/early_hint_preload_test_helper.sys.mjs" + ); + +// TODO testing: +// * Abort main document load while early hint is still loading -> early hint should be aborted + +// Test that with early hint config option disabled, no early hint requests are made +add_task(async function test_103_preload_disabled() { + Services.prefs.setBoolPref("network.early-hints.enabled", false); + await test_hint_preload( + "test_103_preload_disabled", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 0, normal: 1 } + ); + Services.prefs.setBoolPref("network.early-hints.enabled", true); +}); + +add_task(async function test_103_font_disabled() { + let url = + "https://example.com/browser/netwerk/test/browser/early_hint_asset_html.sjs?hinted=1&as=font"; + Services.prefs.setBoolPref("gfx.downloadable_fonts.enabled", false); + await test_preload_url("font_loading_disabled", url, { + hinted: 0, + normal: 0, + }); + Services.prefs.setBoolPref("gfx.downloadable_fonts.enabled", true); + await test_preload_url("font_loading_enabled", url, { + hinted: 1, + normal: 0, + }); + Services.prefs.clearUserPref("gfx.downloadable_fonts.enabled"); +}); + +// Preload with same origin in secure context with mochitest http proxy +add_task(async function test_103_preload_https() { + await test_hint_preload( + "test_103_preload_https", + "https://example.org", + "/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 1, normal: 0 } + ); +}); + +// Preload with same origin in secure context +add_task(async function test_103_preload() { + await test_hint_preload( + "test_103_preload", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 1, normal: 0 } + ); +}); + +// Cross origin preload in secure context +add_task(async function test_103_preload_cor() { + await test_hint_preload( + "test_103_preload_cor", + "https://example.com", + "https://example.net/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 1, normal: 0 } + ); +}); + +// Cross origin preload in insecure context +add_task(async function test_103_preload_insecure_cor() { + await test_hint_preload( + "test_103_preload_insecure_cor", + "https://example.com", + "http://mochi.test:8888/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 0, normal: 1 } + ); +}); + +// Same origin request with relative url +add_task(async function test_103_relative_preload() { + await test_hint_preload( + "test_103_relative_preload", + "https://example.com", + "/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 1, normal: 0 } + ); +}); + +// Early hint from insecure context +add_task(async function test_103_insecure_preload() { + await test_hint_preload( + "test_103_insecure_preload", + "http://mochi.test:8888", + "/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 0, normal: 1 } + ); +}); + +// Cross origin preload from secure context to insecure context on same domain +add_task(async function test_103_preload_mixed_content() { + await test_hint_preload( + "test_103_preload_mixed_content", + "https://example.org", + "http://example.org/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 0, normal: 1 } + ); +}); + +// Same preload from localhost to localhost should preload +add_task(async function test_103_preload_localhost_to_localhost() { + await test_hint_preload( + "test_103_preload_localhost_to_localhost", + "http://127.0.0.1:8888", + "http://127.0.0.1:8888/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 1, normal: 0 } + ); +}); + +// Relative url, correct file for requested uri +add_task(async function test_103_preload_only_file() { + await test_hint_preload( + "test_103_preload_only_file", + "https://example.com", + "early_hint_pixel.sjs", + { hinted: 1, normal: 0 } + ); +}); diff --git a/netwerk/test/browser/browser_103_preload_2.js b/netwerk/test/browser/browser_103_preload_2.js new file mode 100644 index 0000000000..c9f92fdef5 --- /dev/null +++ b/netwerk/test/browser/browser_103_preload_2.js @@ -0,0 +1,180 @@ +/* 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"; + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +const { + test_hint_preload, + test_hint_preload_internal, + request_count_checking, +} = ChromeUtils.importESModule( + "resource://testing-common/early_hint_preload_test_helper.sys.mjs" +); + +// two early hint responses +add_task(async function test_103_two_preload_responses() { + await test_hint_preload_internal( + "103_two_preload_responses", + "https://example.com", + [ + [ + "https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs", + Services.uuid.generateUUID().toString(), + ], + ["", "new_response"], // indicate new early hint response + [ + "https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs", + Services.uuid.generateUUID().toString(), + ], + ], + { hinted: 1, normal: 1 } + ); +}); + +// two link header in one early hint response +add_task(async function test_103_two_link_header() { + await test_hint_preload_internal( + "103_two_link_header", + "https://example.com", + [ + [ + "https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs", + Services.uuid.generateUUID().toString(), + ], + ["", ""], // indicate new link header in same reponse + [ + "https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs", + Services.uuid.generateUUID().toString(), + ], + ], + { hinted: 2, normal: 0 } + ); +}); + +// two links in one early hint link header +add_task(async function test_103_two_links() { + await test_hint_preload_internal( + "103_two_links", + "https://example.com", + [ + [ + "https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs", + Services.uuid.generateUUID().toString(), + ], + [ + "https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs", + Services.uuid.generateUUID().toString(), + ], + ], + { hinted: 2, normal: 0 } + ); +}); + +// two early hint responses, only second one has a link header +add_task(async function test_103_two_links() { + await test_hint_preload_internal( + "103_two_links", + "https://example.com", + [ + ["", "non_link_header"], // indicate non-link related header + ["", "new_response"], // indicate new early hint response + [ + "https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs", + Services.uuid.generateUUID().toString(), + ], + ], + { hinted: 1, normal: 0 } + ); +}); + +// Preload twice same origin in secure context +add_task(async function test_103_preload_twice() { + // pass two times the same uuid so that on the second request, the response is + // already in the cache + let uuid = Services.uuid.generateUUID(); + await test_hint_preload( + "test_103_preload_twice_1", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 1, normal: 0 }, + uuid + ); + await test_hint_preload( + "test_103_preload_twice_2", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 0, normal: 0 }, + uuid + ); +}); + +// Test that preloads in iframes don't get triggered +add_task(async function test_103_iframe() { + // reset the count + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + let iframeUri = + "https://example.com/browser/netwerk/test/browser/103_preload_iframe.html"; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: iframeUri, + waitForLoad: true, + }, + async function () {} + ); + + let gotRequestCount = await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs" + ).then(response => response.json()); + let expectedRequestCount = { hinted: 0, normal: 1 }; + + await request_count_checking( + "test_103_iframe", + gotRequestCount, + expectedRequestCount + ); + + Services.cache2.clear(); +}); + +// Test that anchors are parsed +add_task(async function test_103_anchor() { + // reset the count + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + let anchorUri = + "https://example.com/browser/netwerk/test/browser/103_preload_anchor.html"; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: anchorUri, + waitForLoad: true, + }, + async function () {} + ); + + let gotRequestCount = await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs" + ).then(response => response.json()); + + await request_count_checking("test_103_anchor", gotRequestCount, { + hinted: 0, + normal: 1, + }); +}); diff --git a/netwerk/test/browser/browser_103_private_window.js b/netwerk/test/browser/browser_103_private_window.js new file mode 100644 index 0000000000..7d23ea4b28 --- /dev/null +++ b/netwerk/test/browser/browser_103_private_window.js @@ -0,0 +1,70 @@ +/* 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"; + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +registerCleanupFunction(function () { + Services.prefs.clearUserPref("network.early-hints.enabled"); +}); + +// Test steps: +// 1. Load early_hint_asset_html.sjs with a provided uuid. +// 2. In early_hint_asset_html.sjs, a 103 response with +// a Link header<early_hint_asset.sjs> and the provided uuid is returned. +// 3. We use "http-on-opening-request" topic to observe whether the +// early hinted request is created. +// 4. Finally, we check if the request has the correct `isPrivate` value. +async function test_early_hints_load_url(usePrivateWin) { + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + // Open a browsing window. + const win = await BrowserTestUtils.openNewBrowserWindow({ + private: usePrivateWin, + }); + + let id = Services.uuid.generateUUID().toString(); + let expectedUrl = `https://example.com/browser/netwerk/test/browser/early_hint_asset.sjs?as=fetch&uuid=${id}`; + let observed = {}; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == "http-on-opening-request") { + let channel = aSubject.QueryInterface(Ci.nsIHttpChannel); + if (channel.URI.spec === expectedUrl) { + observed.actrualUrl = channel.URI.spec; + let isPrivate = channel.QueryInterface( + Ci.nsIPrivateBrowsingChannel + ).isChannelPrivate; + observed.isPrivate = isPrivate; + Services.obs.removeObserver(observer, "http-on-opening-request"); + } + } + }, + }; + Services.obs.addObserver(observer, "http-on-opening-request"); + + let requestUrl = `https://example.com/browser/netwerk/test/browser/early_hint_asset_html.sjs?as=fetch&hinted=1&uuid=${id}`; + + const browser = win.gBrowser.selectedTab.linkedBrowser; + let loaded = BrowserTestUtils.browserLoaded(browser, false, requestUrl); + BrowserTestUtils.startLoadingURIString(browser, requestUrl); + await loaded; + + Assert.equal(observed.actrualUrl, expectedUrl); + Assert.equal(observed.isPrivate, usePrivateWin); + + await BrowserTestUtils.closeWindow(win); +} + +add_task(async function test_103_private_window() { + await test_early_hints_load_url(true); + await test_early_hints_load_url(false); +}); diff --git a/netwerk/test/browser/browser_103_redirect.js b/netwerk/test/browser/browser_103_redirect.js new file mode 100644 index 0000000000..d396813d50 --- /dev/null +++ b/netwerk/test/browser/browser_103_redirect.js @@ -0,0 +1,52 @@ +/* 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"; + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +const { test_hint_preload, request_count_checking } = + ChromeUtils.importESModule( + "resource://testing-common/early_hint_preload_test_helper.sys.mjs" + ); + +// Early hint to redirect to same origin in secure context +add_task(async function test_103_redirect_same_origin() { + await test_hint_preload( + "test_103_redirect_same_origin", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_redirect.sjs?https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 2, normal: 0 } // successful preload of redirect and resulting image + ); +}); + +// Early hint to redirect to cross origin in secure context +add_task(async function test_103_redirect_cross_origin() { + await test_hint_preload( + "test_103_redirect_cross_origin", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_redirect.sjs?https://example.net/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 2, normal: 0 } + ); +}); + +// Early hint to redirect to cross origin in insecure context +add_task(async function test_103_redirect_insecure_cross_origin() { + await test_hint_preload( + "test_103_redirect_insecure_cross_origin", + "https://example.com", + "https://example.com/browser/netwerk/test/browser/early_hint_redirect.sjs?http://mochi.test:8888/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 2, normal: 0 } + ); +}); + +// Cross origin preload from secure context to redirected insecure context on same domain +add_task(async function test_103_preload_redirect_mixed_content() { + await test_hint_preload( + "test_103_preload_redirect_mixed_content", + "https://example.org", + "https://example.org/browser/netwerk/test/browser/early_hint_redirect.sjs?http://example.org/browser/netwerk/test/browser/early_hint_pixel.sjs", + { hinted: 2, normal: 0 } + ); +}); diff --git a/netwerk/test/browser/browser_103_redirect_from_server.js b/netwerk/test/browser/browser_103_redirect_from_server.js new file mode 100644 index 0000000000..0357d11516 --- /dev/null +++ b/netwerk/test/browser/browser_103_redirect_from_server.js @@ -0,0 +1,321 @@ +/* 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"; + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +registerCleanupFunction(function () { + Services.prefs.clearUserPref("network.early-hints.enabled"); +}); + +// This function tests Early Hint responses before and in between HTTP redirects. +// +// Arguments: +// - name: String identifying the test case for easier parsing in the log +// - chain and destination: defines the redirect chain, see example below +// note: ALL preloaded urls must be image urls +// - expected: number of normal, cancelled and completed hinted responses. +// +// # Example +// The parameter values of +// ``` +// chain = [ +// {link:"https://link1", host:"https://host1.com"}, +// {link:"https://link2", host:"https://host2.com"}, +// ] +// ``` +// and `destination = "https://host3.com/page.html" would result in the +// following HTTP exchange (simplified): +// +// ``` +// > GET https://host1.com/redirect?something1 +// +// < 103 Early Hints +// < Link: <https://link1>;rel=preload;as=image +// < +// < 307 Temporary Redirect +// < Location: https://host2.com/redirect?something2 +// < +// +// > GET https://host2.com/redirect?something2 +// +// < 103 Early Hints +// < Link: <https://link2>;rel=preload;as=image +// < +// < 307 Temporary Redirect +// < Location: https://host3.com/page.html +// < +// +// > GET https://host3.com/page.html +// +// < [...] Result depends on the final page +// ``` +// +// Legend: +// * `>` indicates a request going from client to server +// * `<` indicates a response going from server to client +// * all lines are terminated with a `\r\n` +// +async function test_hint_redirect( + name, + chain, + destination, + hint_destination, + expected +) { + // pass the full redirect chain as a url parameter. Each redirect is handled + // by `early_hint_redirect_html.sjs` which url-decodes the query string and + // redirects to the result + let links = []; + let url = destination; + for (let i = chain.length - 1; i >= 0; i--) { + let qp = new URLSearchParams(); + if (chain[i].link != "") { + qp.append("link", "<" + chain[i].link + ">;rel=preload;as=image"); + links.push(chain[i].link); + } + qp.append("location", url); + + url = `${ + chain[i].host + }/browser/netwerk/test/browser/early_hint_redirect_html.sjs?${qp.toString()}`; + } + if (hint_destination != "") { + links.push(hint_destination); + } + + // reset the count + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + // main request and all other must get their respective OnStopRequest + let numRequestRemaining = + expected.normal + expected.hinted + expected.cancelled; + let observed = { + hinted: 0, + normal: 0, + cancelled: 0, + }; + // store channelIds + let observedChannelIds = []; + let callback; + let promise = new Promise(resolve => { + callback = resolve; + }); + if (numRequestRemaining > 0) { + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + aSubject.QueryInterface(Ci.nsIIdentChannel); + let id = aSubject.channelId; + if (observedChannelIds.includes(id)) { + return; + } + aSubject.QueryInterface(Ci.nsIRequest); + dump("Observer aSubject.name " + aSubject.name + "\n"); + if (aTopic == "http-on-stop-request" && links.includes(aSubject.name)) { + if (aSubject.status == Cr.NS_ERROR_ABORT) { + observed.cancelled += 1; + } else { + aSubject.QueryInterface(Ci.nsIHttpChannel); + let initiator = ""; + try { + initiator = aSubject.getRequestHeader("X-Moz"); + } catch {} + if (initiator == "early hint") { + observed.hinted += 1; + } else { + observed.normal += 1; + } + } + observedChannelIds.push(id); + numRequestRemaining -= 1; + dump("Observer numRequestRemaining " + numRequestRemaining + "\n"); + } + if (numRequestRemaining == 0) { + Services.obs.removeObserver(observer, "http-on-stop-request"); + callback(); + } + }, + }; + Services.obs.addObserver(observer, "http-on-stop-request"); + } else { + callback(); + } + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url, + waitForLoad: true, + }, + async function () {} + ); + + // wait until all requests are stopped, especially the cancelled ones + await promise; + + let got = await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs" + ).then(response => response.json()); + + // stringify to pretty print assert output + let g = JSON.stringify(observed); + let e = JSON.stringify(expected); + Assert.equal( + expected.normal, + observed.normal, + `${name} normal observed from client expected ${expected.normal} (${e}) got ${observed.normal} (${g})` + ); + Assert.equal( + expected.hinted, + observed.hinted, + `${name} hinted observed from client expected ${expected.hinted} (${e}) got ${observed.hinted} (${g})` + ); + Assert.equal( + expected.cancelled, + observed.cancelled, + `${name} cancelled observed from client expected ${expected.cancelled} (${e}) got ${observed.cancelled} (${g})` + ); + + // each cancelled request might be cancelled after the request was already + // made. Allow cancelled responses to count towards the hinted to avoid + // intermittent test failures. + Assert.ok( + expected.hinted <= got.hinted && + got.hinted <= expected.hinted + expected.cancelled, + `${name}: unexpected amount of hinted request made got ${ + got.hinted + }, expected between ${expected.hinted} and ${ + expected.hinted + expected.cancelled + }` + ); + Assert.ok( + got.normal == expected.normal, + `${name}: unexpected amount of normal request made expected ${expected.normal}, got ${got.normal}` + ); + Assert.equal(numRequestRemaining, 0, "Requests remaining"); +} + +add_task(async function double_redirect_cross_origin() { + await test_hint_redirect( + "double_redirect_cross_origin_both_hints", + [ + { + link: "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + host: "https://example.com/", + }, + { + link: "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + host: "https://example.net", + }, + ], + "https://example.org/browser/netwerk/test/browser/early_hint_asset_html.sjs?as=image&hinted=1", + "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + { hinted: 1, normal: 0, cancelled: 2 } + ); + await test_hint_redirect( + "double_redirect_second_hint", + [ + { + link: "", + host: "https://example.com/", + }, + { + link: "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + host: "https://example.net", + }, + ], + "https://example.org/browser/netwerk/test/browser/early_hint_asset_html.sjs?as=image&hinted=1", + "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + { hinted: 1, normal: 0, cancelled: 1 } + ); + await test_hint_redirect( + "double_redirect_first_hint", + [ + { + link: "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + host: "https://example.com/", + }, + { + link: "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + host: "https://example.net", + }, + ], + "https://example.org/browser/netwerk/test/browser/early_hint_asset_html.sjs?as=image&hinted=0", + "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + { hinted: 0, normal: 1, cancelled: 2 } + ); +}); + +add_task(async function redirect_cross_origin() { + await test_hint_redirect( + "redirect_cross_origin_start_second_preload", + [ + { + link: "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + host: "https://example.net", + }, + ], + "https://example.org/browser/netwerk/test/browser/early_hint_asset_html.sjs?as=image&hinted=1", + "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + { hinted: 1, normal: 0, cancelled: 1 } + ); + await test_hint_redirect( + "redirect_cross_origin_dont_use_first_preload", + [ + { + link: "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image&a", + host: "https://example.net", + }, + ], + "https://example.org/browser/netwerk/test/browser/early_hint_asset_html.sjs?as=image&hinted=0", + "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + { hinted: 0, normal: 1, cancelled: 1 } + ); +}); + +add_task(async function redirect_same_origin() { + await test_hint_redirect( + "hint_before_redirect_same_origin", + [ + { + link: "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + host: "https://example.org", + }, + ], + "https://example.org/browser/netwerk/test/browser/early_hint_asset_html.sjs?as=image&hinted=1", + "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + { hinted: 1, normal: 0, cancelled: 0 } + ); + await test_hint_redirect( + "hint_after_redirect_same_origin", + [ + { + link: "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + host: "https://example.org", + }, + ], + "https://example.org/browser/netwerk/test/browser/early_hint_asset_html.sjs?as=image&hinted=0", + "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + { hinted: 1, normal: 0, cancelled: 0 } + ); + await test_hint_redirect( + "hint_after_redirect_same_origin", + [ + { + link: "", + host: "https://example.org", + }, + ], + "https://example.org/browser/netwerk/test/browser/early_hint_asset_html.sjs?as=image&hinted=1", + "https://example.org/browser/netwerk/test/browser/early_hint_asset.sjs?as=image", + { hinted: 1, normal: 0, cancelled: 0 } + ); +}); diff --git a/netwerk/test/browser/browser_103_referrer_policy.js b/netwerk/test/browser/browser_103_referrer_policy.js new file mode 100644 index 0000000000..646b7255fd --- /dev/null +++ b/netwerk/test/browser/browser_103_referrer_policy.js @@ -0,0 +1,167 @@ +/* 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/. */ + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +async function test_referrer_policy(input, expected_results) { + // reset the count + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_referrer_policy_html.sjs?action=reset_referrer_results" + ); + + let requestUrl = `https://example.com/browser/netwerk/test/browser/early_hint_referrer_policy_html.sjs?as=${ + input.resource_type + }&hinted=${input.hinted ? "1" : "0"}${ + input.header_referrer_policy + ? "&header_referrer_policy=" + input.header_referrer_policy + : "" + } + ${ + input.link_referrer_policy + ? "&link_referrer_policy=" + input.link_referrer_policy + : "" + }`; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: requestUrl, + waitForLoad: true, + }, + async function () {} + ); + + let gotRequestCount = await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs" + ).then(response => response.json()); + + // Retrieve the request referrer from the server + let referrer_response = await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_referrer_policy_html.sjs?action=get_request_referrer_results" + ).then(response => response.text()); + + Assert.ok( + referrer_response === expected_results.referrer, + "Request referrer matches expected - " + input.test_name + ); + + await Assert.deepEqual( + gotRequestCount, + { hinted: expected_results.hinted, normal: expected_results.normal }, + `${input.testName} (${input.resource_type}): Unexpected amount of requests made` + ); +} + +add_task(async function test_103_referrer_policies() { + let tests = [ + { + input: { + test_name: "image - no policies", + resource_type: "image", + header_referrer_policy: "", + link_referrer_policy: "", + hinted: true, + }, + expected: { + hinted: 1, + normal: 0, + referrer: + "https://example.com/browser/netwerk/test/browser/early_hint_referrer_policy_html.sjs?as=image&hinted=1", + }, + }, + { + input: { + test_name: "image - origin on header", + resource_type: "image", + header_referrer_policy: "origin", + link_referrer_policy: "", + hinted: true, + }, + expected: { hinted: 1, normal: 0, referrer: "https://example.com/" }, + }, + { + input: { + test_name: "image - origin on link", + resource_type: "image", + header_referrer_policy: "", + link_referrer_policy: "origin", + hinted: true, + }, + expected: { hinted: 1, normal: 0, referrer: "https://example.com/" }, + }, + { + input: { + test_name: "image - origin on both", + resource_type: "image", + header_referrer_policy: "origin", + link_referrer_policy: "origin", + hinted: true, + }, + expected: { hinted: 1, normal: 0, referrer: "https://example.com/" }, + }, + { + input: { + test_name: "image - no-referrer on header", + resource_type: "image", + header_referrer_policy: "no-referrer", + link_referrer_policy: "", + hinted: true, + }, + expected: { hinted: 1, normal: 0, referrer: "" }, + }, + { + input: { + test_name: "image - no-referrer on link", + resource_type: "image", + header_referrer_policy: "", + link_referrer_policy: "no-referrer", + hinted: true, + }, + expected: { hinted: 1, normal: 0, referrer: "" }, + }, + { + input: { + test_name: "image - no-referrer on both", + resource_type: "image", + header_referrer_policy: "no-referrer", + link_referrer_policy: "no-referrer", + hinted: true, + }, + expected: { hinted: 1, normal: 0, referrer: "" }, + }, + { + // link referrer policy takes precedence + input: { + test_name: "image - origin on header, no-referrer on link", + resource_type: "image", + header_referrer_policy: "origin", + link_referrer_policy: "no-referrer", + hinted: true, + }, + expected: { hinted: 1, normal: 0, referrer: "" }, + }, + { + // link referrer policy takes precedence + input: { + test_name: "image - no-referrer on header, origin on link", + resource_type: "image", + header_referrer_policy: "no-referrer", + link_referrer_policy: "origin", + hinted: true, + }, + expected: { hinted: 1, normal: 0, referrer: "https://example.com/" }, + }, + ]; + + for (let test of tests) { + await test_referrer_policy(test.input, test.expected); + } +}); diff --git a/netwerk/test/browser/browser_103_telemetry.js b/netwerk/test/browser/browser_103_telemetry.js new file mode 100644 index 0000000000..c6cc7dd070 --- /dev/null +++ b/netwerk/test/browser/browser_103_telemetry.js @@ -0,0 +1,107 @@ +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +Services.prefs.setCharPref("dom.securecontext.allowlist", "example.com"); + +var kTest103 = + "https://example.com/browser/netwerk/test/browser/103_preload.html"; +var kTestNo103 = + "https://example.com/browser/netwerk/test/browser/no_103_preload.html"; +var kTest404 = + "https://example.com/browser/netwerk/test/browser/103_preload_and_404.html"; + +add_task(async function () { + let hist_hints = TelemetryTestUtils.getAndClearHistogram( + "EH_NUM_OF_HINTS_PER_PAGE" + ); + let hist_final = TelemetryTestUtils.getAndClearHistogram("EH_FINAL_RESPONSE"); + let hist_time = TelemetryTestUtils.getAndClearHistogram( + "EH_TIME_TO_FINAL_RESPONSE" + ); + + await BrowserTestUtils.openNewForegroundTab(gBrowser, kTest103, true); + + // This is a 200 response with 103: + // EH_NUM_OF_HINTS_PER_PAGE should record 1. + // EH_FINAL_RESPONSE should record 1 on position 0 (R2xx). + // EH_TIME_TO_FINAL_RESPONSE should have a new value + // (we cannot determine what the timing will be therefore we only check that + // the histogram sum is > 0). + TelemetryTestUtils.assertHistogram(hist_hints, 1, 1); + TelemetryTestUtils.assertHistogram(hist_final, 0, 1); + const snapshot = hist_time.snapshot(); + let found = false; + for (let [val] of Object.entries(snapshot.values)) { + if (val > 0) { + found = true; + } + } + Assert.ok(found); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function () { + let hist_hints = TelemetryTestUtils.getAndClearHistogram( + "EH_NUM_OF_HINTS_PER_PAGE" + ); + let hist_final = TelemetryTestUtils.getAndClearHistogram("EH_FINAL_RESPONSE"); + let hist_time = TelemetryTestUtils.getAndClearHistogram( + "EH_TIME_TO_FINAL_RESPONSE" + ); + + await BrowserTestUtils.openNewForegroundTab(gBrowser, kTestNo103, true); + + // This is a 200 response without 103: + // EH_NUM_OF_HINTS_PER_PAGE should record 0. + // EH_FINAL_RESPONSE andd EH_TIME_TO_FINAL_RESPONSE should not be recorded. + TelemetryTestUtils.assertHistogram(hist_hints, 0, 1); + const snapshot_final = hist_final.snapshot(); + Assert.equal(snapshot_final.sum, 0); + const snapshot_time = hist_time.snapshot(); + let found = false; + for (let [val] of Object.entries(snapshot_time.values)) { + if (val > 0) { + found = true; + } + } + Assert.ok(!found); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function () { + let hist_hints = TelemetryTestUtils.getAndClearHistogram( + "EH_NUM_OF_HINTS_PER_PAGE" + ); + let hist_final = TelemetryTestUtils.getAndClearHistogram("EH_FINAL_RESPONSE"); + let hist_time = TelemetryTestUtils.getAndClearHistogram( + "EH_TIME_TO_FINAL_RESPONSE" + ); + + await BrowserTestUtils.openNewForegroundTab(gBrowser, kTest404, true); + + // This is a 404 response with 103: + // EH_NUM_OF_HINTS_PER_PAGE and EH_TIME_TO_FINAL_RESPONSE should not be recorded. + // EH_FINAL_RESPONSE should record 1 on index 2 (R4xx). + const snapshot_hints = hist_hints.snapshot(); + Assert.equal(snapshot_hints.sum, 0); + TelemetryTestUtils.assertHistogram(hist_final, 2, 1); + const snapshot_time = hist_time.snapshot(); + let found = false; + for (let [val] of Object.entries(snapshot_time.values)) { + if (val > 0) { + found = true; + } + } + Assert.ok(!found); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function cleanup() { + Services.prefs.clearUserPref("dom.securecontext.allowlist"); +}); diff --git a/netwerk/test/browser/browser_103_user_load.js b/netwerk/test/browser/browser_103_user_load.js new file mode 100644 index 0000000000..94a30e1c8c --- /dev/null +++ b/netwerk/test/browser/browser_103_user_load.js @@ -0,0 +1,85 @@ +/* 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"; + +// simulate user initiated loads by entering the URL in the URL-bar code based on +// https://searchfox.org/mozilla-central/rev/5644fae86d5122519a0e34ee03117c88c6ed9b47/browser/components/urlbar/tests/browser/browser_enter.js + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +const { + request_count_checking, + test_hint_preload_internal, + test_hint_preload, +} = ChromeUtils.importESModule( + "resource://testing-common/early_hint_preload_test_helper.sys.mjs" +); + +const START_VALUE = + "https://example.com/browser/netwerk/test/browser/early_hint_asset_html.sjs?as=style&hinted=1"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); +}); + +Services.prefs.setBoolPref("network.early-hints.enabled", true); + +// bug 1780822 +add_task(async function user_initiated_load() { + // reset the count + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + info("Simple user initiated load"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // under normal test conditions using the systemPrincipal as loadingPrincipal + // doesn't elicit a crash, changing the behavior for this test: + // https://searchfox.org/mozilla-central/rev/5644fae86d5122519a0e34ee03117c88c6ed9b47/dom/security/nsContentSecurityManager.cpp#1149-1150 + Services.prefs.setBoolPref( + "security.disallow_non_local_systemprincipal_in_tests", + true + ); + + gURLBar.value = START_VALUE; + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + // reset the config option + Services.prefs.clearUserPref( + "security.disallow_non_local_systemprincipal_in_tests" + ); + + // Check url bar and selected tab. + is( + gURLBar.value, + UrlbarTestUtils.trimURL(START_VALUE), + "Urlbar should preserve the value on return keypress" + ); + is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab"); + + let gotRequestCount = await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs" + ).then(response => response.json()); + let expectedRequestCount = { hinted: 1, normal: 0 }; + + await request_count_checking( + "test_preload_user_initiated", + gotRequestCount, + expectedRequestCount + ); + + // Cleanup. + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/netwerk/test/browser/browser_NetUtil.js b/netwerk/test/browser/browser_NetUtil.js new file mode 100644 index 0000000000..09875a5280 --- /dev/null +++ b/netwerk/test/browser/browser_NetUtil.js @@ -0,0 +1,111 @@ +/* +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ +*/ +"use strict"; + +function test() { + waitForExplicitFinish(); + + // We overload this test to include verifying that httpd.js is + // importable as a testing-only JS module. + ChromeUtils.importESModule("resource://testing-common/httpd.sys.mjs"); + + nextTest(); +} + +function nextTest() { + if (tests.length) { + executeSoon(tests.shift()); + } else { + executeSoon(finish); + } +} + +var tests = [test_asyncFetchBadCert]; + +function test_asyncFetchBadCert() { + // Try a load from an untrusted cert, with errors supressed + NetUtil.asyncFetch( + { + uri: "https://untrusted.example.com", + loadUsingSystemPrincipal: true, + }, + function (aInputStream, aStatusCode, aRequest) { + ok(!Components.isSuccessCode(aStatusCode), "request failed"); + ok(aRequest instanceof Ci.nsIHttpChannel, "request is an nsIHttpChannel"); + + // Now try again with a channel whose notificationCallbacks doesn't suprress errors + let channel = NetUtil.newChannel({ + uri: "https://untrusted.example.com", + loadUsingSystemPrincipal: true, + }); + channel.notificationCallbacks = { + QueryInterface: ChromeUtils.generateQI([ + "nsIProgressEventSink", + "nsIInterfaceRequestor", + ]), + getInterface(aIID) { + return this.QueryInterface(aIID); + }, + onProgress() {}, + onStatus() {}, + }; + NetUtil.asyncFetch( + channel, + function (aInputStream, aStatusCode, aRequest) { + ok(!Components.isSuccessCode(aStatusCode), "request failed"); + ok( + aRequest instanceof Ci.nsIHttpChannel, + "request is an nsIHttpChannel" + ); + + // Now try a valid request + NetUtil.asyncFetch( + { + uri: "https://example.com", + loadUsingSystemPrincipal: true, + }, + function (aInputStream, aStatusCode, aRequest) { + info("aStatusCode for valid request: " + aStatusCode); + ok(Components.isSuccessCode(aStatusCode), "request succeeded"); + ok( + aRequest instanceof Ci.nsIHttpChannel, + "request is an nsIHttpChannel" + ); + ok(aRequest.requestSucceeded, "HTTP request succeeded"); + + nextTest(); + } + ); + } + ); + } + ); +} + +function WindowListener(aURL, aCallback) { + this.callback = aCallback; + this.url = aURL; +} +WindowListener.prototype = { + onOpenWindow(aXULWindow) { + var domwindow = aXULWindow.docShell.domWindow; + var self = this; + domwindow.addEventListener( + "load", + function () { + if (domwindow.document.location.href != self.url) { + return; + } + + // Allow other window load listeners to execute before passing to callback + executeSoon(function () { + self.callback(domwindow); + }); + }, + { once: true } + ); + }, + onCloseWindow(aXULWindow) {}, +}; diff --git a/netwerk/test/browser/browser_about_cache.js b/netwerk/test/browser/browser_about_cache.js new file mode 100644 index 0000000000..9e9b2467d5 --- /dev/null +++ b/netwerk/test/browser/browser_about_cache.js @@ -0,0 +1,136 @@ +"use strict"; + +/** + * Open a dummy page, then open about:cache and verify the opened page shows up in the cache. + */ +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.network_state", false]], + }); + + const kRoot = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" + ); + const kTestPage = kRoot + "dummy.html"; + // Open the dummy page to get it cached. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + kTestPage, + true + ); + BrowserTestUtils.removeTab(tab); + + tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:cache", + true + ); + let expectedPageCheck = function (uri) { + info("Saw load for " + uri); + // Can't easily use searchParms and new URL() because it's an about: URI... + return uri.startsWith("about:cache?") && uri.includes("storage=disk"); + }; + let diskPageLoaded = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedPageCheck + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + ok( + !content.document.nodePrincipal.isSystemPrincipal, + "about:cache should not have system principal" + ); + let principal = content.document.nodePrincipal; + let channel = content.docShell.currentDocumentChannel; + ok(!channel.loadInfo.loadingPrincipal, "Loading principal should be null."); + is( + principal.spec, + content.document.location.href, + "Principal matches location" + ); + let links = [...content.document.querySelectorAll("a[href*=disk]")]; + is(links.length, 1, "Should have 1 link to the disk entries"); + links[0].click(); + }); + await diskPageLoaded; + info("about:cache disk subpage loaded"); + + expectedPageCheck = function (uri) { + info("Saw load for " + uri); + return uri.startsWith("about:cache-entry") && uri.includes("dummy.html"); + }; + let triggeringURISpec = tab.linkedBrowser.currentURI.spec; + let entryLoaded = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedPageCheck + ); + await SpecialPowers.spawn( + tab.linkedBrowser, + [kTestPage], + function (kTestPage) { + ok( + !content.document.nodePrincipal.isSystemPrincipal, + "about:cache with query params should still not have system principal" + ); + let principal = content.document.nodePrincipal; + is( + principal.spec, + content.document.location.href, + "Principal matches location" + ); + let channel = content.docShell.currentDocumentChannel; + principal = channel.loadInfo.triggeringPrincipal; + is( + principal.spec, + "about:cache", + "Triggering principal matches previous location" + ); + ok( + !channel.loadInfo.loadingPrincipal, + "Loading principal should be null." + ); + let links = [ + ...content.document.querySelectorAll("a[href*='" + kTestPage + "']"), + ]; + is(links.length, 1, "Should have 1 link to the entry for " + kTestPage); + links[0].click(); + } + ); + await entryLoaded; + info("about:cache entry loaded"); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [triggeringURISpec], + function (triggeringURISpec) { + ok( + !content.document.nodePrincipal.isSystemPrincipal, + "about:cache-entry should also not have system principal" + ); + let principal = content.document.nodePrincipal; + is( + principal.spec, + content.document.location.href, + "Principal matches location" + ); + let channel = content.docShell.currentDocumentChannel; + principal = channel.loadInfo.triggeringPrincipal; + is( + principal.spec, + triggeringURISpec, + "Triggering principal matches previous location" + ); + ok( + !channel.loadInfo.loadingPrincipal, + "Loading principal should be null." + ); + ok( + content.document.querySelectorAll("th").length, + "Should have several table headers with data." + ); + } + ); + BrowserTestUtils.removeTab(tab); +}); diff --git a/netwerk/test/browser/browser_backgroundtask_purgeHTTPCache.js b/netwerk/test/browser/browser_backgroundtask_purgeHTTPCache.js new file mode 100644 index 0000000000..76af1451f5 --- /dev/null +++ b/netwerk/test/browser/browser_backgroundtask_purgeHTTPCache.js @@ -0,0 +1,40 @@ +/* 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"; + +add_task(async function test_startupCleanup() { + Services.prefs.setBoolPref( + "network.cache.shutdown_purge_in_background_task", + true + ); + Services.prefs.setBoolPref("privacy.clearOnShutdown.cache", true); + Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true); + let dir = Services.dirsvc.get("ProfD", Ci.nsIFile); + dir.append("cache2.2021-11-25-08-47-04.purge.bg_rm"); + Assert.equal(dir.exists(), false, `Folder ${dir.path} should not exist`); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + Assert.equal( + dir.exists(), + true, + `Folder ${dir.path} should have been created` + ); + + Services.obs.notifyObservers(null, "browser-delayed-startup-finished"); + + await TestUtils.waitForCondition(() => { + return !dir.exists(); + }); + + Assert.equal( + dir.exists(), + false, + `Folder ${dir.path} should have been purged by background task` + ); + Services.prefs.clearUserPref( + "network.cache.shutdown_purge_in_background_task" + ); + Services.prefs.clearUserPref("privacy.clearOnShutdown.cache"); + Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown"); +}); diff --git a/netwerk/test/browser/browser_bug1535877.js b/netwerk/test/browser/browser_bug1535877.js new file mode 100644 index 0000000000..0bd0a98d11 --- /dev/null +++ b/netwerk/test/browser/browser_bug1535877.js @@ -0,0 +1,15 @@ +"use strict"; + +add_task(_ => { + try { + Cc["@mozilla.org/network/effective-tld-service;1"].createInstance( + Ci.nsISupports + ); + } catch (e) { + is( + e.result, + Cr.NS_ERROR_XPC_CI_RETURNED_FAILURE, + "Component creation as an instance fails with expected code" + ); + } +}); diff --git a/netwerk/test/browser/browser_bug1629307.js b/netwerk/test/browser/browser_bug1629307.js new file mode 100644 index 0000000000..03ea2476e2 --- /dev/null +++ b/netwerk/test/browser/browser_bug1629307.js @@ -0,0 +1,81 @@ +/* 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"; + +// Load a web page containing an iframe that requires authentication but includes the X-Frame-Options: SAMEORIGIN header. +// Make sure that we don't needlessly show an authentication prompt for it. + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +add_task(async function () { + SpecialPowers.pushPrefEnv({ + set: [["network.auth.supress_auth_prompt_for_XFO_failures", true]], + }); + + let URL = + "https://example.com/browser/netwerk/test/browser/test_1629307.html"; + + let hasPrompt = false; + + PromptTestUtils.handleNextPrompt( + window, + { + modalType: Services.prefs.getIntPref("prompts.modalType.httpAuth"), + promptType: "promptUserAndPass", + }, + { buttonNumClick: 1 } + ) + .then(function () { + hasPrompt = true; + }) + .catch(function () {}); + + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, URL); + + // wait until the page and its iframe page is loaded + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, true, URL); + + Assert.equal( + hasPrompt, + false, + "no prompt when loading page via iframe with x-auth options" + ); +}); + +add_task(async function () { + SpecialPowers.pushPrefEnv({ + set: [["network.auth.supress_auth_prompt_for_XFO_failures", false]], + }); + + let URL = + "https://example.com/browser/netwerk/test/browser/test_1629307.html"; + + let hasPrompt = false; + + PromptTestUtils.handleNextPrompt( + window, + { + modalType: Services.prefs.getIntPref("prompts.modalType.httpAuth"), + promptType: "promptUserAndPass", + }, + { buttonNumClick: 1 } + ) + .then(function () { + hasPrompt = true; + }) + .catch(function () {}); + + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, URL); + + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, true, URL); + + Assert.equal( + hasPrompt, + true, + "prompt when loading page via iframe with x-auth options with pref network.auth.supress_auth_prompt_for_XFO_failures disabled" + ); +}); diff --git a/netwerk/test/browser/browser_child_resource.js b/netwerk/test/browser/browser_child_resource.js new file mode 100644 index 0000000000..341a8fc8e3 --- /dev/null +++ b/netwerk/test/browser/browser_child_resource.js @@ -0,0 +1,246 @@ +/* +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ +*/ +"use strict"; + +// This must be loaded in the remote process for this test to be useful +const TEST_URL = "https://example.com/browser/netwerk/test/browser/dummy.html"; + +const expectedRemote = gMultiProcessBrowser ? "true" : ""; + +const resProtocol = Cc[ + "@mozilla.org/network/protocol;1?name=resource" +].getService(Ci.nsIResProtocolHandler); + +function waitForEvent(obj, name, capturing, chromeEvent) { + info("Waiting for " + name); + return new Promise(resolve => { + function listener(event) { + info("Saw " + name); + obj.removeEventListener(name, listener, capturing, chromeEvent); + resolve(event); + } + + obj.addEventListener(name, listener, capturing, chromeEvent); + }); +} + +function resolveURI(uri) { + uri = Services.io.newURI(uri); + try { + return resProtocol.resolveURI(uri); + } catch (e) { + return null; + } +} + +function remoteResolveURI(uri) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [uri], uriToResolve => { + let resProtocol = Cc[ + "@mozilla.org/network/protocol;1?name=resource" + ].getService(Ci.nsIResProtocolHandler); + + uriToResolve = Services.io.newURI(uriToResolve); + try { + return resProtocol.resolveURI(uriToResolve); + } catch (e) {} + return null; + }); +} + +// Restarts the child process by crashing it then reloading the tab +var restart = async function () { + let browser = gBrowser.selectedBrowser; + // If the tab isn't remote this would crash the main process so skip it + if (browser.getAttribute("remote") != "true") { + return browser; + } + + await BrowserTestUtils.crashFrame(browser); + + browser.reload(); + + await BrowserTestUtils.browserLoaded(browser); + is( + browser.getAttribute("remote"), + expectedRemote, + "Browser should be in the right process" + ); + return browser; +}; + +// Sanity check that this test is going to be useful +add_task(async function () { + await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // This must be loaded in the remote process for this test to be useful + is( + gBrowser.selectedBrowser.getAttribute("remote"), + expectedRemote, + "Browser should be in the right process" + ); + + let local = resolveURI("resource://gre/modules/AppConstants.jsm"); + let remote = await remoteResolveURI( + "resource://gre/modules/AppConstants.jsm" + ); + is(local, remote, "AppConstants.jsm should resolve in both processes"); + + gBrowser.removeCurrentTab(); +}); + +// Add a mapping, update it then remove it +add_task(async function () { + await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + info("Set"); + resProtocol.setSubstitution( + "testing", + Services.io.newURI("chrome://global/content") + ); + let local = resolveURI("resource://testing/test.js"); + let remote = await remoteResolveURI("resource://testing/test.js"); + is( + local, + "chrome://global/content/test.js", + "Should resolve in main process" + ); + is( + remote, + "chrome://global/content/test.js", + "Should resolve in child process" + ); + + info("Change"); + resProtocol.setSubstitution( + "testing", + Services.io.newURI("chrome://global/skin") + ); + local = resolveURI("resource://testing/test.js"); + remote = await remoteResolveURI("resource://testing/test.js"); + is(local, "chrome://global/skin/test.js", "Should resolve in main process"); + is(remote, "chrome://global/skin/test.js", "Should resolve in child process"); + + info("Clear"); + resProtocol.setSubstitution("testing", null); + local = resolveURI("resource://testing/test.js"); + remote = await remoteResolveURI("resource://testing/test.js"); + is(local, null, "Shouldn't resolve in main process"); + is(remote, null, "Shouldn't resolve in child process"); + + gBrowser.removeCurrentTab(); +}); + +// Add a mapping, restart the child process then check it is still there +add_task(async function () { + await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + info("Set"); + resProtocol.setSubstitution( + "testing", + Services.io.newURI("chrome://global/content") + ); + let local = resolveURI("resource://testing/test.js"); + let remote = await remoteResolveURI("resource://testing/test.js"); + is( + local, + "chrome://global/content/test.js", + "Should resolve in main process" + ); + is( + remote, + "chrome://global/content/test.js", + "Should resolve in child process" + ); + + await restart(); + + local = resolveURI("resource://testing/test.js"); + remote = await remoteResolveURI("resource://testing/test.js"); + is( + local, + "chrome://global/content/test.js", + "Should resolve in main process" + ); + is( + remote, + "chrome://global/content/test.js", + "Should resolve in child process" + ); + + info("Change"); + resProtocol.setSubstitution( + "testing", + Services.io.newURI("chrome://global/skin") + ); + + await restart(); + + local = resolveURI("resource://testing/test.js"); + remote = await remoteResolveURI("resource://testing/test.js"); + is(local, "chrome://global/skin/test.js", "Should resolve in main process"); + is(remote, "chrome://global/skin/test.js", "Should resolve in child process"); + + info("Clear"); + resProtocol.setSubstitution("testing", null); + + await restart(); + + local = resolveURI("resource://testing/test.js"); + remote = await remoteResolveURI("resource://testing/test.js"); + is(local, null, "Shouldn't resolve in main process"); + is(remote, null, "Shouldn't resolve in child process"); + + gBrowser.removeCurrentTab(); +}); + +// Adding a mapping to a resource URI should work +add_task(async function () { + await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + info("Set"); + resProtocol.setSubstitution( + "testing", + Services.io.newURI("chrome://global/content") + ); + resProtocol.setSubstitution( + "testing2", + Services.io.newURI("resource://testing") + ); + let local = resolveURI("resource://testing2/test.js"); + let remote = await remoteResolveURI("resource://testing2/test.js"); + is( + local, + "chrome://global/content/test.js", + "Should resolve in main process" + ); + is( + remote, + "chrome://global/content/test.js", + "Should resolve in child process" + ); + + info("Clear"); + resProtocol.setSubstitution("testing", null); + local = resolveURI("resource://testing2/test.js"); + remote = await remoteResolveURI("resource://testing2/test.js"); + is( + local, + "chrome://global/content/test.js", + "Should resolve in main process" + ); + is( + remote, + "chrome://global/content/test.js", + "Should resolve in child process" + ); + + resProtocol.setSubstitution("testing2", null); + local = resolveURI("resource://testing2/test.js"); + remote = await remoteResolveURI("resource://testing2/test.js"); + is(local, null, "Shouldn't resolve in main process"); + is(remote, null, "Shouldn't resolve in child process"); + + gBrowser.removeCurrentTab(); +}); diff --git a/netwerk/test/browser/browser_cookie_filtering_basic.js b/netwerk/test/browser/browser_cookie_filtering_basic.js new file mode 100644 index 0000000000..e51bed5bc5 --- /dev/null +++ b/netwerk/test/browser/browser_cookie_filtering_basic.js @@ -0,0 +1,184 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { + HTTPS_EXAMPLE_ORG, + HTTPS_EXAMPLE_COM, + HTTP_EXAMPLE_COM, + browserTestPath, + waitForAllExpectedTests, + cleanupObservers, + checkExpectedCookies, + fetchHelper, + preclean_test, + cleanup_test, +} = ChromeUtils.importESModule( + "resource://testing-common/cookie_filtering_helper.sys.mjs" +); + +// run suite with content listener +// 1. initializes the content process and observer +// 2. runs the test gamut +// 3. cleans up the content process +async function runSuiteWithContentListener(name, triggerSuiteFunc, expected) { + return async function (browser) { + info("Running content suite: " + name); + await SpecialPowers.spawn(browser, [expected, name], checkExpectedCookies); + await triggerSuiteFunc(); + await SpecialPowers.spawn(browser, [], waitForAllExpectedTests); + await SpecialPowers.spawn(browser, [], cleanupObservers); + info("Complete content suite: " + name); + }; +} + +// TEST: Different domains (org) +// * example.org cookies go to example.org process +// * exmaple.com cookies do not go to example.org process +async function test_basic_suite_org() { + // example.org - start content process when loading page + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: browserTestPath(HTTPS_EXAMPLE_ORG), + }, + await runSuiteWithContentListener( + "basic suite org", + triggerBasicSuite, + basicSuiteMatchingDomain(HTTPS_EXAMPLE_ORG) + ) + ); +} + +// TEST: Different domains (com) +// * example.com cookies go to example.com process +// * example.org cookies do not go to example.com process +// * insecure example.com cookies go to secure com process +async function test_basic_suite_com() { + // example.com - start content process when loading page + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: browserTestPath(HTTPS_EXAMPLE_COM), + }, + await runSuiteWithContentListener( + "basic suite com", + triggerBasicSuite, + basicSuiteMatchingDomain(HTTPS_EXAMPLE_COM).concat( + basicSuiteMatchingDomain(HTTP_EXAMPLE_COM) + ) + ) + ); +} + +// TEST: Duplicate domain (org) +// * example.org cookies go to multiple example.org processes +async function test_basic_suite_org_duplicate() { + let expected = basicSuiteMatchingDomain(HTTPS_EXAMPLE_ORG); + let t1 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + browserTestPath(HTTPS_EXAMPLE_ORG) + ); + let testStruct1 = { + name: "example.org primary", + browser: gBrowser.getBrowserForTab(t1), + tab: t1, + expected, + }; + + // example.org dup + let t3 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + browserTestPath(HTTPS_EXAMPLE_ORG) + ); + let testStruct3 = { + name: "example.org dup", + browser: gBrowser.getBrowserForTab(t3), + tab: t3, + expected, + }; + + let parentpid = Services.appinfo.processID; + let pid1 = testStruct1.browser.frameLoader.remoteTab.osPid; + let pid3 = testStruct3.browser.frameLoader.remoteTab.osPid; + ok( + parentpid != pid1, + "Parent pid should differ from content process for 1st example.org" + ); + ok( + parentpid != pid3, + "Parent pid should differ from content process for 2nd example.org" + ); + ok(pid1 != pid3, "Content pids should differ from each other"); + + await SpecialPowers.spawn( + testStruct1.browser, + [testStruct1.expected, testStruct1.name], + checkExpectedCookies + ); + + await SpecialPowers.spawn( + testStruct3.browser, + [testStruct3.expected, testStruct3.name], + checkExpectedCookies + ); + + await triggerBasicSuite(); + + // wait for all tests and cleanup + await SpecialPowers.spawn(testStruct1.browser, [], waitForAllExpectedTests); + await SpecialPowers.spawn(testStruct3.browser, [], waitForAllExpectedTests); + await SpecialPowers.spawn(testStruct1.browser, [], cleanupObservers); + await SpecialPowers.spawn(testStruct3.browser, [], cleanupObservers); + BrowserTestUtils.removeTab(testStruct1.tab); + BrowserTestUtils.removeTab(testStruct3.tab); +} + +function basicSuite() { + var suite = []; + suite.push(["test-cookie=aaa", HTTPS_EXAMPLE_ORG]); + suite.push(["test-cookie=bbb", HTTPS_EXAMPLE_ORG]); + suite.push(["test-cookie=dad", HTTPS_EXAMPLE_ORG]); + suite.push(["test-cookie=rad", HTTPS_EXAMPLE_ORG]); + suite.push(["test-cookie=orgwontsee", HTTPS_EXAMPLE_COM]); + suite.push(["test-cookie=sentinelorg", HTTPS_EXAMPLE_ORG]); + suite.push(["test-cookie=sentinelcom", HTTPS_EXAMPLE_COM]); + return suite; +} + +function basicSuiteMatchingDomain(domain) { + var suite = basicSuite(); + var result = []; + for (var [cookie, dom] of suite) { + if (dom == domain) { + result.push(cookie); + } + } + return result; +} + +// triggers set-cookie, which will trigger cookie-changed messages +// messages will be filtered against the cookie list created from above +// only unfiltered messages should make it to the content process +async function triggerBasicSuite() { + let triggerCookies = basicSuite(); + for (var [cookie, domain] of triggerCookies) { + let secure = false; + if (domain.includes("https")) { + secure = true; + } + + //trigger + var url = browserTestPath(domain) + "cookie_filtering_resource.sjs"; + await fetchHelper(url, cookie, secure); + } +} + +add_task(preclean_test); +add_task(test_basic_suite_org); // 5 +add_task(test_basic_suite_com); // 2 +add_task(test_basic_suite_org_duplicate); // 13 +add_task(cleanup_test); diff --git a/netwerk/test/browser/browser_cookie_filtering_cross_origin.js b/netwerk/test/browser/browser_cookie_filtering_cross_origin.js new file mode 100644 index 0000000000..5a722846ef --- /dev/null +++ b/netwerk/test/browser/browser_cookie_filtering_cross_origin.js @@ -0,0 +1,146 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +const { + HTTPS_EXAMPLE_ORG, + HTTPS_EXAMPLE_COM, + HTTP_EXAMPLE_COM, + browserTestPath, + waitForAllExpectedTests, + cleanupObservers, + checkExpectedCookies, + preclean_test, + cleanup_test, +} = ChromeUtils.importESModule( + "resource://testing-common/cookie_filtering_helper.sys.mjs" +); + +async function runSuiteWithContentListener(name, trigger_suite_func, expected) { + return async function (browser) { + info("Running content suite: " + name); + await SpecialPowers.spawn(browser, [expected, name], checkExpectedCookies); + await trigger_suite_func(); + await SpecialPowers.spawn(browser, [], waitForAllExpectedTests); + await SpecialPowers.spawn(browser, [], cleanupObservers); + info("Complete content suite: " + name); + }; +} + +// TEST: Cross Origin Resource (com) +// * process receives only COR cookies pertaining to same page +async function test_cross_origin_resource_com() { + let comExpected = []; + comExpected.push("test-cookie=comhtml"); // 1 + comExpected.push("test-cookie=png"); // 2 + comExpected.push("test-cookie=orghtml"); // 3 + // nothing for 4, 5, 6, 7 -> goes to .org process + comExpected.push("test-cookie=png"); // 8 + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: browserTestPath(HTTPS_EXAMPLE_COM), + }, + await runSuiteWithContentListener( + "COR example.com", + triggerCrossOriginSuite, + comExpected + ) + ); + Services.cookies.removeAll(); +} + +// TEST: Cross Origin Resource (org) +// * received COR cookies only pertaining to the process's page +async function test_cross_origin_resource_org() { + let orgExpected = []; + // nothing for 1, 2 and 3 -> goes to .com + orgExpected.push("test-cookie=png"); // 4 + orgExpected.push("test-cookie=orghtml"); // 5 + orgExpected.push("test-cookie=png"); // 6 + orgExpected.push("test-cookie=comhtml"); // 7 + // 8 nothing for 8 -> goes to .com process + orgExpected.push("test-cookie=png"); // 9. Sentinel to verify no more came in + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: browserTestPath(HTTPS_EXAMPLE_ORG), + }, + await runSuiteWithContentListener( + "COR example.org", + triggerCrossOriginSuite, + orgExpected + ) + ); +} + +// seems to block better than fetch for secondary resource +// using for more predicatable recving +async function requestBrowserPageWithFilename( + testName, + requestFrom, + filename, + param = "" +) { + let url = requestFrom + "/browser/netwerk/test/browser/" + filename; + + // add param if necessary + if (param != "") { + url += "?" + param; + } + + info("requesting " + url + " (" + testName + ")"); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url, + }, + async () => {} + ); +} + +async function triggerCrossOriginSuite() { + // SameSite - 1 com page, 2 com png + await requestBrowserPageWithFilename( + "SameSite resource (com)", + HTTPS_EXAMPLE_COM, + "cookie_filtering_secure_resource_com.html" + ); + + // COR - 3 com page, 4 org png + await requestBrowserPageWithFilename( + "COR (com-org)", + HTTPS_EXAMPLE_COM, + "cookie_filtering_secure_resource_org.html" + ); + + // SameSite - 5 org page, 6 org png + await requestBrowserPageWithFilename( + "SameSite resource (org)", + HTTPS_EXAMPLE_ORG, + "cookie_filtering_secure_resource_org.html" + ); + + // COR - 7 org page, 8 com png + await requestBrowserPageWithFilename( + "SameSite resource (org-com)", + HTTPS_EXAMPLE_ORG, + "cookie_filtering_secure_resource_com.html" + ); + + // Sentinel to verify no more cookies come in after last true test + await requestBrowserPageWithFilename( + "COR sentinel", + HTTPS_EXAMPLE_ORG, + "cookie_filtering_square.png" + ); +} + +add_task(preclean_test); +add_task(test_cross_origin_resource_com); // 4 +add_task(test_cross_origin_resource_org); // 5 +add_task(cleanup_test); diff --git a/netwerk/test/browser/browser_cookie_filtering_insecure.js b/netwerk/test/browser/browser_cookie_filtering_insecure.js new file mode 100644 index 0000000000..679bfc5a56 --- /dev/null +++ b/netwerk/test/browser/browser_cookie_filtering_insecure.js @@ -0,0 +1,106 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +const { + HTTPS_EXAMPLE_ORG, + HTTPS_EXAMPLE_COM, + HTTP_EXAMPLE_COM, + browserTestPath, + waitForAllExpectedTests, + cleanupObservers, + checkExpectedCookies, + fetchHelper, + preclean_test, + cleanup_test, +} = ChromeUtils.importESModule( + "resource://testing-common/cookie_filtering_helper.sys.mjs" +); + +async function runSuiteWithContentListener(name, trigger_suite_func, expected) { + return async function (browser) { + info("Running content suite: " + name); + await SpecialPowers.spawn(browser, [expected, name], checkExpectedCookies); + await trigger_suite_func(); + await SpecialPowers.spawn(browser, [], waitForAllExpectedTests); + await SpecialPowers.spawn(browser, [], cleanupObservers); + info("Complete content suite: " + name); + }; +} + +// TEST: In/Secure (insecure com) +// * secure example.com cookie do not go to insecure example.com process +// * insecure cookies go to insecure process +// * secure request with insecure cookie will go to insecure process +async function test_insecure_suite_insecure_com() { + var expected = []; + expected.push("test-cookie=png1"); + expected.push("test-cookie=png2"); + // insecure com will not recieve the secure com request with secure cookie + expected.push(""); // insecure com will lose visibility of secure com cookie + expected.push("test-cookie=png3"); + info(expected); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: browserTestPath(HTTP_EXAMPLE_COM), + }, + await runSuiteWithContentListener( + "insecure suite insecure com", + triggerInsecureSuite, + expected + ) + ); +} + +// TEST: In/Secure (secure com) +// * secure page will recieve all secure/insecure cookies +async function test_insecure_suite_secure_com() { + var expected = []; + expected.push("test-cookie=png1"); + expected.push("test-cookie=png2"); + expected.push("test-cookie=secure-png"); + expected.push("test-cookie=png3"); + info(expected); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: browserTestPath(HTTPS_EXAMPLE_COM), + }, + await runSuiteWithContentListener( + "insecure suite secure com", + triggerInsecureSuite, + expected + ) + ); +} + +async function triggerInsecureSuite() { + const cookieSjsFilename = "cookie_filtering_resource.sjs"; + + // insecure page, insecure cookie + var url = browserTestPath(HTTP_EXAMPLE_COM) + cookieSjsFilename; + await fetchHelper(url, "test-cookie=png1", false); + + // secure page req, insecure cookie + url = browserTestPath(HTTPS_EXAMPLE_COM) + cookieSjsFilename; + await fetchHelper(url, "test-cookie=png2", false); + + // secure page, secure cookie + url = browserTestPath(HTTPS_EXAMPLE_COM) + cookieSjsFilename; + await fetchHelper(url, "test-cookie=secure-png", true); + + // not testing insecure server returning secure cookie -- + + // sentinel + url = browserTestPath(HTTPS_EXAMPLE_COM) + cookieSjsFilename; + await fetchHelper(url, "test-cookie=png3", false); +} + +add_task(preclean_test); +add_task(test_insecure_suite_insecure_com); // 3 +add_task(test_insecure_suite_secure_com); // 4 +add_task(cleanup_test); diff --git a/netwerk/test/browser/browser_cookie_filtering_oa.js b/netwerk/test/browser/browser_cookie_filtering_oa.js new file mode 100644 index 0000000000..f69ad09e81 --- /dev/null +++ b/netwerk/test/browser/browser_cookie_filtering_oa.js @@ -0,0 +1,190 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { + HTTPS_EXAMPLE_ORG, + HTTPS_EXAMPLE_COM, + HTTP_EXAMPLE_COM, + browserTestPath, + waitForAllExpectedTests, + cleanupObservers, + checkExpectedCookies, + triggerSetCookieFromHttp, + triggerSetCookieFromHttpPrivate, + preclean_test, + cleanup_test, +} = ChromeUtils.importESModule( + "resource://testing-common/cookie_filtering_helper.sys.mjs" +); + +// TEST: OriginAttributes +// * example.com OA-changed cookies don't go to example.com & vice-versa +async function test_origin_attributes() { + var suite = oaSuite(); + + // example.com - start content process when loading page + let t2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + browserTestPath(HTTPS_EXAMPLE_COM) + ); + let testStruct2 = { + name: "OA example.com", + browser: gBrowser.getBrowserForTab(t2), + tab: t2, + expected: [suite[0], suite[4]], + }; + + // open a tab with altered OA: userContextId + let t4 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: (function () { + return function () { + // info("calling addTab from lambda"); + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + HTTPS_EXAMPLE_COM, + { userContextId: 1 } + ); + }; + })(), + }); + let testStruct4 = { + name: "OA example.com (contextId)", + browser: gBrowser.getBrowserForTab(t4), + tab: t4, + expected: [suite[2], suite[5]], + }; + + // example.com + await SpecialPowers.spawn( + testStruct2.browser, + [testStruct2.expected, testStruct2.name], + checkExpectedCookies + ); + // example.com with different OA: userContextId + await SpecialPowers.spawn( + testStruct4.browser, + [testStruct4.expected, testStruct4.name], + checkExpectedCookies + ); + + await triggerOriginAttributesEmulatedSuite(); + + await SpecialPowers.spawn(testStruct2.browser, [], waitForAllExpectedTests); + await SpecialPowers.spawn(testStruct4.browser, [], waitForAllExpectedTests); + await SpecialPowers.spawn(testStruct2.browser, [], cleanupObservers); + await SpecialPowers.spawn(testStruct4.browser, [], cleanupObservers); + BrowserTestUtils.removeTab(testStruct2.tab); + BrowserTestUtils.removeTab(testStruct4.tab); +} + +// TEST: Private +// * example.com private cookies don't go to example.com process & vice-v +async function test_private() { + var suite = oaSuite(); + + // example.com + let t2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + browserTestPath(HTTPS_EXAMPLE_COM) + ); + let testStruct2 = { + name: "non-priv example.com", + browser: gBrowser.getBrowserForTab(t2), + tab: t2, + expected: [suite[0], suite[4]], + }; + + // private window example.com + let privateBrowserWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let privateTab = (privateBrowserWindow.gBrowser.selectedTab = + BrowserTestUtils.addTab( + privateBrowserWindow.gBrowser, + browserTestPath(HTTPS_EXAMPLE_COM) + )); + let testStruct5 = { + name: "private example.com", + browser: privateBrowserWindow.gBrowser.getBrowserForTab(privateTab), + tab: privateTab, + expected: [suite[3], suite[6]], + }; + await BrowserTestUtils.browserLoaded(testStruct5.tab.linkedBrowser); + + let parentpid = Services.appinfo.processID; + let privatePid = testStruct5.browser.frameLoader.remoteTab.osPid; + let pid = testStruct2.browser.frameLoader.remoteTab.osPid; + ok(parentpid != privatePid, "Parent and private processes are unique"); + ok(parentpid != pid, "Parent and non-private processes are unique"); + ok(privatePid != pid, "Private and non-private processes are unique"); + + // example.com + await SpecialPowers.spawn( + testStruct2.browser, + [testStruct2.expected, testStruct2.name], + checkExpectedCookies + ); + + // example.com private + await SpecialPowers.spawn( + testStruct5.browser, + [testStruct5.expected, testStruct5.name], + checkExpectedCookies + ); + + await triggerOriginAttributesEmulatedSuite(); + + // wait for all tests and cleanup + await SpecialPowers.spawn(testStruct2.browser, [], waitForAllExpectedTests); + await SpecialPowers.spawn(testStruct5.browser, [], waitForAllExpectedTests); + await SpecialPowers.spawn(testStruct2.browser, [], cleanupObservers); + await SpecialPowers.spawn(testStruct5.browser, [], cleanupObservers); + BrowserTestUtils.removeTab(testStruct2.tab); + BrowserTestUtils.removeTab(testStruct5.tab); + await BrowserTestUtils.closeWindow(privateBrowserWindow); +} + +function oaSuite() { + var suite = []; + suite.push("test-cookie=orgwontsee"); // 0 + suite.push("test-cookie=firstparty"); // 1 + suite.push("test-cookie=usercontext"); // 2 + suite.push("test-cookie=privateonly"); // 3 + suite.push("test-cookie=sentinelcom"); // 4 + suite.push("test-cookie=sentineloa"); // 5 + suite.push("test-cookie=sentinelprivate"); // 6 + return suite; +} + +// emulated because we are not making actual page requests +// we are just directly invoking the api +async function triggerOriginAttributesEmulatedSuite() { + var suite = oaSuite(); + + let uriCom = NetUtil.newURI(HTTPS_EXAMPLE_COM); + triggerSetCookieFromHttp(uriCom, suite[0]); + + // FPD (OA) changed + triggerSetCookieFromHttp(uriCom, suite[1], "foo.com"); + + // context id (OA) changed + triggerSetCookieFromHttp(uriCom, suite[2], "", 1); + + // private + triggerSetCookieFromHttpPrivate(uriCom, suite[3]); + + // sentinels + triggerSetCookieFromHttp(uriCom, suite[4]); + triggerSetCookieFromHttp(uriCom, suite[5], "", 1); + triggerSetCookieFromHttpPrivate(uriCom, suite[6]); +} + +add_task(preclean_test); +add_task(test_origin_attributes); // 4 +add_task(test_private); // 7 +add_task(cleanup_test); diff --git a/netwerk/test/browser/browser_cookie_filtering_subdomain.js b/netwerk/test/browser/browser_cookie_filtering_subdomain.js new file mode 100644 index 0000000000..78fcdb07dd --- /dev/null +++ b/netwerk/test/browser/browser_cookie_filtering_subdomain.js @@ -0,0 +1,175 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { + HTTPS_EXAMPLE_ORG, + HTTPS_EXAMPLE_COM, + HTTP_EXAMPLE_COM, + browserTestPath, + waitForAllExpectedTests, + cleanupObservers, + checkExpectedCookies, + fetchHelper, + preclean_test, + cleanup_test, +} = ChromeUtils.importESModule( + "resource://testing-common/cookie_filtering_helper.sys.mjs" +); + +const HTTPS_SUBDOMAIN_1_EXAMPLE_COM = "https://test1.example.com"; +const HTTP_SUBDOMAIN_1_EXAMPLE_COM = "http://test1.example.com"; +const HTTPS_SUBDOMAIN_2_EXAMPLE_COM = "https://test2.example.com"; +const HTTP_SUBDOMAIN_2_EXAMPLE_COM = "http://test2.example.com"; + +// run suite with content listener +// 1. initializes the content process and observer +// 2. runs the test gamut +// 3. cleans up the content process +async function runSuiteWithContentListener(name, triggerSuiteFunc, expected) { + return async function (browser) { + info("Running content suite: " + name); + await SpecialPowers.spawn(browser, [expected, name], checkExpectedCookies); + await triggerSuiteFunc(); + await SpecialPowers.spawn(browser, [], waitForAllExpectedTests); + await SpecialPowers.spawn(browser, [], cleanupObservers); + info("Complete content suite: " + name); + }; +} + +// TEST: domain receives subdomain cookies +async function test_domain() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: browserTestPath(HTTPS_EXAMPLE_COM), + }, + await runSuiteWithContentListener( + "test_domain", + triggerSuite, + cookiesFromSuite() + ) + ); +} + +// TEST: insecure domain receives base and sub-domain insecure cookies +async function test_insecure_domain() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: browserTestPath(HTTP_EXAMPLE_COM), + }, + + await runSuiteWithContentListener("test_insecure_domain", triggerSuite, [ + "", + "", // HTTPS fetch cookies show as empty strings + "test-cookie-insecure=insecure_domain", + "test-cookie-insecure=insecure_subdomain", + "", + ]) + ); +} + +// TEST: subdomain receives base domain and other sub-domain cookies +async function test_subdomain() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: browserTestPath(HTTPS_SUBDOMAIN_2_EXAMPLE_COM), + }, + await runSuiteWithContentListener( + "test_subdomain", + triggerSuite, + cookiesFromSuite() + ) + ); +} + +// TEST: insecure subdomain receives base and sub-domain insecure cookies +async function test_insecure_subdomain() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: browserTestPath(HTTP_SUBDOMAIN_2_EXAMPLE_COM), + }, + await runSuiteWithContentListener( + "test_insecure_subdomain", + triggerSuite, + + [ + "", + "", // HTTPS fetch cookies show as empty strings + "test-cookie-insecure=insecure_domain", + "test-cookie-insecure=insecure_subdomain", + "", + ] + ) + ); +} + +function suite() { + var suite = []; + suite.push(["test-cookie=domain", HTTPS_EXAMPLE_COM]); + suite.push(["test-cookie=subdomain", HTTPS_SUBDOMAIN_1_EXAMPLE_COM]); + suite.push(["test-cookie-insecure=insecure_domain", HTTP_EXAMPLE_COM]); + suite.push([ + "test-cookie-insecure=insecure_subdomain", + HTTP_SUBDOMAIN_1_EXAMPLE_COM, + ]); + suite.push(["test-cookie=sentinel", HTTPS_EXAMPLE_COM]); + return suite; +} + +function cookiesFromSuite() { + var cookies = []; + for (var [cookie] of suite()) { + cookies.push(cookie); + } + return cookies; +} + +function cookiesMatchingDomain(domain) { + var s = suite(); + var result = []; + for (var [cookie, dom] of s) { + if (dom == domain) { + result.push(cookie); + } + } + return result; +} + +function justSitename(maybeSchemefulMaybeSubdomainSite) { + let mssArray = maybeSchemefulMaybeSubdomainSite.split("://"); + let maybesubdomain = mssArray[mssArray.length - 1]; + let msdArray = maybesubdomain.split("."); + return msdArray.slice(msdArray.length - 2, msdArray.length).join("."); +} + +// triggers set-cookie, which will trigger cookie-changed messages +// messages will be filtered against the cookie list created from above +// only unfiltered messages should make it to the content process +async function triggerSuite() { + let triggerCookies = suite(); + for (var [cookie, schemefulDomain] of triggerCookies) { + let secure = false; + if (schemefulDomain.includes("https")) { + secure = true; + } + + var url = + browserTestPath(schemefulDomain) + "cookie_filtering_resource.sjs"; + await fetchHelper(url, cookie, secure, justSitename(schemefulDomain)); + Services.cookies.removeAll(); // clean cookies across secure/insecure runs + } +} + +add_task(preclean_test); +add_task(test_domain); // 5 +add_task(test_insecure_domain); // 2 +add_task(test_subdomain); // 5 +add_task(test_insecure_subdomain); // 2 +add_task(cleanup_test); diff --git a/netwerk/test/browser/browser_cookie_sync_across_tabs.js b/netwerk/test/browser/browser_cookie_sync_across_tabs.js new file mode 100644 index 0000000000..127bb2555b --- /dev/null +++ b/netwerk/test/browser/browser_cookie_sync_across_tabs.js @@ -0,0 +1,79 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +add_task(async function () { + info("Make sure cookie changes in one process are visible in the other"); + + const kRoot = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" + ); + const kTestPage = kRoot + "dummy.html"; + + Services.cookies.removeAll(); + + let tab1 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: kTestPage, + forceNewProcess: true, + }); + let tab2 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: kTestPage, + forceNewProcess: true, + }); + + let browser1 = gBrowser.getBrowserForTab(tab1); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let pid1 = browser1.frameLoader.remoteTab.osPid; + let pid2 = browser2.frameLoader.remoteTab.osPid; + + // Note, this might not be true once fission is implemented (Bug 1451850) + ok(pid1 != pid2, "We should have different processes here."); + + await SpecialPowers.spawn(browser1, [], async function () { + is(content.document.cookie, "", "Expecting no cookies"); + }); + + await SpecialPowers.spawn(browser2, [], async function () { + is(content.document.cookie, "", "Expecting no cookies"); + }); + + await SpecialPowers.spawn(browser1, [], async function () { + content.document.cookie = "a1=test"; + }); + + await SpecialPowers.spawn(browser2, [], async function () { + is(content.document.cookie, "a1=test", "Cookie should be set"); + content.document.cookie = "a1=other_test"; + }); + + await SpecialPowers.spawn(browser1, [], async function () { + is(content.document.cookie, "a1=other_test", "Cookie should be set"); + content.document.cookie = "a2=again"; + }); + + await SpecialPowers.spawn(browser2, [], async function () { + is( + content.document.cookie, + "a1=other_test; a2=again", + "Cookie should be set" + ); + content.document.cookie = "a1=; expires=Thu, 01-Jan-1970 00:00:01 GMT;"; + content.document.cookie = "a2=; expires=Thu, 01-Jan-1970 00:00:01 GMT;"; + }); + + await SpecialPowers.spawn(browser1, [], async function () { + is(content.document.cookie, "", "Cookies should be cleared"); + }); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + + ok(true, "Got to the end of the test!"); +}); diff --git a/netwerk/test/browser/browser_fetch_lnk.js b/netwerk/test/browser/browser_fetch_lnk.js new file mode 100644 index 0000000000..ea8ef57984 --- /dev/null +++ b/netwerk/test/browser/browser_fetch_lnk.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async () => { + const FILE_PAGE = Services.io.newFileURI( + new FileUtils.File(getTestFilePath("dummy.html")) + ).spec; + await BrowserTestUtils.withNewTab(FILE_PAGE, async browser => { + try { + await SpecialPowers.spawn(browser, [], () => + content.fetch("./file_lnk.lnk") + ); + ok( + false, + "Loading lnk must fail if it links to a file from other directory" + ); + } catch (err) { + is(err.constructor.name, "TypeError", "Should fail on Windows"); + } + }); +}); diff --git a/netwerk/test/browser/browser_http_index_format.js b/netwerk/test/browser/browser_http_index_format.js new file mode 100644 index 0000000000..7aa2f84a86 --- /dev/null +++ b/netwerk/test/browser/browser_http_index_format.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const FILE_URI = Services.appinfo.OS == "WINNT" ? "file://C:/" : "file:///"; + +const RESOURCE_URI = "resource:///"; + +const ZIP_FILE = "res_empty.zip"; +const ZIP_DIR = getChromeDir(getResolvedURI(gTestPath)); +ZIP_DIR.append(ZIP_FILE); +ZIP_DIR.normalize(); +const ZIP_URI = Services.io.newFileURI(ZIP_DIR).spec; +const JAR_URI = "jar:" + ZIP_URI + "!/"; + +const BASE_URI = "http://mochi.test:8888/browser/netwerk/test/browser/"; +const INDEX_URI = BASE_URI + "res_http_index_format"; + +async function expectIndexToDisplay(aUrl, aExpectToDisplay) { + info(`Opening ${aUrl} to check if index will be displayed`); + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, aUrl, true); + await SpecialPowers.spawn( + tab.linkedBrowser, + [aExpectToDisplay], + aExpectToDisplay => { + is( + !!content.document.getElementsByTagName("table").length, + aExpectToDisplay, + "The index should be displayed" + ); + } + ); + await BrowserTestUtils.removeTab(tab); +} + +add_task(async function test_http_index_format() { + await expectIndexToDisplay(FILE_URI, true); + await expectIndexToDisplay(RESOURCE_URI, true); + await expectIndexToDisplay(JAR_URI, true); + await expectIndexToDisplay(INDEX_URI, false); + + await SpecialPowers.pushPrefEnv({ + set: [["network.http_index_format.allowed_schemes", "*"]], + }); + + await expectIndexToDisplay(INDEX_URI, true); +}); diff --git a/netwerk/test/browser/browser_nsIFormPOSTActionChannel.js b/netwerk/test/browser/browser_nsIFormPOSTActionChannel.js new file mode 100644 index 0000000000..e791794579 --- /dev/null +++ b/netwerk/test/browser/browser_nsIFormPOSTActionChannel.js @@ -0,0 +1,273 @@ +/* + * Tests for bug 1241377: A channel with nsIFormPOSTActionChannel interface + * should be able to accept form POST. + */ + +"use strict"; + +const SCHEME = "x-bug1241377"; + +const FORM_BASE = SCHEME + "://dummy/form/"; +const NORMAL_FORM_URI = FORM_BASE + "normal.html"; +const UPLOAD_FORM_URI = FORM_BASE + "upload.html"; +const POST_FORM_URI = FORM_BASE + "post.html"; + +const ACTION_BASE = SCHEME + "://dummy/action/"; +const NORMAL_ACTION_URI = ACTION_BASE + "normal.html"; +const UPLOAD_ACTION_URI = ACTION_BASE + "upload.html"; +const POST_ACTION_URI = ACTION_BASE + "post.html"; + +function CustomProtocolHandler() {} +CustomProtocolHandler.prototype = { + /** nsIProtocolHandler */ + get scheme() { + return SCHEME; + }, + newChannel(aURI, aLoadInfo) { + return new CustomChannel(aURI, aLoadInfo); + }, + allowPort(port, scheme) { + return port != -1; + }, + + /** nsISupports */ + QueryInterface: ChromeUtils.generateQI(["nsIProtocolHandler"]), +}; + +function CustomChannel(aURI, aLoadInfo) { + this.uri = aURI; + this.loadInfo = aLoadInfo; + + this._uploadStream = null; + + var interfaces = [Ci.nsIRequest, Ci.nsIChannel]; + if (this.uri.spec == POST_ACTION_URI) { + interfaces.push(Ci.nsIFormPOSTActionChannel); + } else if (this.uri.spec == UPLOAD_ACTION_URI) { + interfaces.push(Ci.nsIUploadChannel); + } + this.QueryInterface = ChromeUtils.generateQI(interfaces); +} +CustomChannel.prototype = { + /** nsIUploadChannel */ + get uploadStream() { + return this._uploadStream; + }, + set uploadStream(val) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + setUploadStream(aStream, aContentType, aContentLength) { + this._uploadStream = aStream; + }, + + /** nsIChannel */ + get originalURI() { + return this.uri; + }, + get URI() { + return this.uri; + }, + owner: null, + notificationCallbacks: null, + get securityInfo() { + return null; + }, + get contentType() { + return "text/html"; + }, + set contentType(val) {}, + contentCharset: "UTF-8", + get contentLength() { + return -1; + }, + set contentLength(val) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + open() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + asyncOpen(aListener) { + var data = ` +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>test bug 1241377</title> +</head> +<body> +`; + + if (this.uri.spec.startsWith(FORM_BASE)) { + data += ` +<form id="form" action="${this.uri.spec.replace(FORM_BASE, ACTION_BASE)}" + method="post" enctype="text/plain" target="frame"> +<input type="hidden" name="foo" value="bar"> +<input type="submit"> +</form> + +<iframe id="frame" name="frame" width="200" height="200"></iframe> + +<script type="text/javascript"> +<!-- +document.getElementById('form').submit(); +//--> +</script> +`; + } else if (this.uri.spec.startsWith(ACTION_BASE)) { + var postData = ""; + var headers = {}; + if (this._uploadStream) { + var bstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + bstream.setInputStream(this._uploadStream); + postData = bstream.readBytes(bstream.available()); + + if (this._uploadStream instanceof Ci.nsIMIMEInputStream) { + this._uploadStream.visitHeaders((name, value) => { + headers[name] = value; + }); + } + } + data += ` +<input id="upload_stream" value="${this._uploadStream ? "yes" : "no"}"> +<input id="post_data" value="${btoa(postData)}"> +<input id="upload_headers" value='${JSON.stringify(headers)}'> +`; + } + + data += ` +</body> +</html> +`; + + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setData(data, data.length); + + var runnable = { + run: () => { + try { + aListener.onStartRequest(this, null); + } catch (e) {} + try { + aListener.onDataAvailable(this, stream, 0, stream.available()); + } catch (e) {} + try { + aListener.onStopRequest(this, null, Cr.NS_OK); + } catch (e) {} + }, + }; + Services.tm.dispatchToMainThread(runnable); + }, + + /** nsIRequest */ + get name() { + return this.uri.spec; + }, + isPending() { + return false; + }, + get status() { + return Cr.NS_OK; + }, + cancel(status) {}, + loadGroup: null, + loadFlags: + Ci.nsIRequest.LOAD_NORMAL | + Ci.nsIRequest.INHIBIT_CACHING | + Ci.nsIRequest.LOAD_BYPASS_CACHE, +}; + +function frameScript() { + /* eslint-env mozilla/frame-script */ + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + addMessageListener("Test:WaitForIFrame", function () { + var check = function () { + if (content) { + var frame = content.document.getElementById("frame"); + if (frame) { + var upload_stream = + frame.contentDocument.getElementById("upload_stream"); + var post_data = frame.contentDocument.getElementById("post_data"); + var headers = frame.contentDocument.getElementById("upload_headers"); + if (upload_stream && post_data && headers) { + sendAsyncMessage("Test:IFrameLoaded", [ + upload_stream.value, + post_data.value, + headers.value, + ]); + return; + } + } + } + + setTimeout(check, 100); + }; + + check(); + }); + /* eslint-enable mozilla/no-arbitrary-setTimeout */ +} + +function loadTestTab(uri) { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, uri); + var browser = gBrowser.selectedBrowser; + + let manager = browser.messageManager; + browser.messageManager.loadFrameScript( + "data:,(" + frameScript.toString() + ")();", + true + ); + + return new Promise(resolve => { + function listener({ data: [hasUploadStream, postData, headers] }) { + manager.removeMessageListener("Test:IFrameLoaded", listener); + resolve([hasUploadStream, atob(postData), JSON.parse(headers)]); + } + + manager.addMessageListener("Test:IFrameLoaded", listener); + manager.sendAsyncMessage("Test:WaitForIFrame"); + }); +} + +add_task(async function () { + var handler = new CustomProtocolHandler(); + Services.io.registerProtocolHandler( + SCHEME, + handler, + Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE, + -1 + ); + registerCleanupFunction(function () { + Services.io.unregisterProtocolHandler(SCHEME); + }); +}); + +add_task(async function () { + var [hasUploadStream] = await loadTestTab(NORMAL_FORM_URI); + is(hasUploadStream, "no", "normal action should not have uploadStream"); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function () { + var [hasUploadStream] = await loadTestTab(UPLOAD_FORM_URI); + is(hasUploadStream, "no", "upload action should not have uploadStream"); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function () { + var [hasUploadStream, postData, headers] = await loadTestTab(POST_FORM_URI); + + is(hasUploadStream, "yes", "post action should have uploadStream"); + is(postData, "foo=bar\r\n", "POST data is received correctly"); + + is(headers["Content-Type"], "text/plain", "Content-Type header is correct"); + is(headers["Content-Length"], undefined, "Content-Length header is correct"); + + gBrowser.removeCurrentTab(); +}); diff --git a/netwerk/test/browser/browser_post_auth.js b/netwerk/test/browser/browser_post_auth.js new file mode 100644 index 0000000000..24104f96d6 --- /dev/null +++ b/netwerk/test/browser/browser_post_auth.js @@ -0,0 +1,65 @@ +"use strict"; + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +const FOLDER = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" +); + +add_task(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + `${FOLDER}post.html` + ); + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + `${FOLDER}post.html` + ); + await browserLoadedPromise; + + let finalLoadPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + `${FOLDER}auth_post.sjs` + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let file = new content.File( + [new content.Blob(["1234".repeat(1024 * 500)], { type: "text/plain" })], + "test-name" + ); + content.document.getElementById("input_file").mozSetFileArray([file]); + content.document.getElementById("form").submit(); + }); + + let promptPromise = PromptTestUtils.handleNextPrompt( + tab.linkedBrowser, + { + modalType: Services.prefs.getIntPref("prompts.modalType.httpAuth"), + promptType: "promptUserAndPass", + }, + { buttonNumClick: 0, loginInput: "user", passwordInput: "pass" } + ); + + await promptPromise; + + await finalLoadPromise; + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + Assert.ok(content.location.href.includes("auth_post.sjs")); + Assert.ok(content.document.body.innerHTML.includes("1234")); + }); + + BrowserTestUtils.removeTab(tab); + + // Clean up any active logins we added during the test. + Services.obs.notifyObservers(null, "net:clear-active-logins"); +}); diff --git a/netwerk/test/browser/browser_post_file.js b/netwerk/test/browser/browser_post_file.js new file mode 100644 index 0000000000..8b156dff9e --- /dev/null +++ b/netwerk/test/browser/browser_post_file.js @@ -0,0 +1,71 @@ +/* + * Tests for bug 1241100: Post to local file should not overwrite the file. + */ +"use strict"; + +async function createTestFile(filename, content) { + let path = PathUtils.join(PathUtils.tempDir, filename); + await IOUtils.writeUTF8(path, content); + return path; +} + +add_task(async function () { + var postFilename = "post_file.html"; + var actionFilename = "action_file.html"; + + var postFileContent = ` +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>post file</title> +</head> +<body onload="document.getElementById('form').submit();"> +<form id="form" action="${actionFilename}" method="post" enctype="text/plain" target="frame"> +<input type="hidden" name="foo" value="bar"> +<input type="submit"> +</form> +<iframe id="frame" name="frame"></iframe> +</body> +</html> +`; + + var actionFileContent = ` +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>action file</title> +</head> +<body> +<div id="action_file_ok">ok</div> +</body> +</html> +`; + + var postPath = await createTestFile(postFilename, postFileContent); + var actionPath = await createTestFile(actionFilename, actionFileContent); + + var postURI = PathUtils.toFileURI(postPath); + var actionURI = PathUtils.toFileURI(actionPath); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + actionURI + ); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, postURI); + await browserLoadedPromise; + + var actionFileContentAfter = await IOUtils.readUTF8(actionPath); + is(actionFileContentAfter, actionFileContent, "action file is not modified"); + + await IOUtils.remove(postPath); + await IOUtils.remove(actionPath); + + gBrowser.removeCurrentTab(); +}); diff --git a/netwerk/test/browser/browser_purgeCache_idle_daily.js b/netwerk/test/browser/browser_purgeCache_idle_daily.js new file mode 100644 index 0000000000..21ba46af1d --- /dev/null +++ b/netwerk/test/browser/browser_purgeCache_idle_daily.js @@ -0,0 +1,86 @@ +/* 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"; + +add_task(async function test_idle_cleanup() { + Services.fog.testResetFOG(); + Services.prefs.setBoolPref( + "network.cache.shutdown_purge_in_background_task", + true + ); + Services.prefs.setBoolPref("privacy.clearOnShutdown.cache", true); + Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true); + let dir = Services.dirsvc.get("ProfD", Ci.nsIFile); + dir.append("cache2.2021-11-25-08-47-04.purge.bg_rm"); + Assert.equal(dir.exists(), false, `Folder ${dir.path} should not exist`); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + Assert.equal( + dir.exists(), + true, + `Folder ${dir.path} should have been created` + ); + + Services.obs.notifyObservers(null, "idle-daily"); + + await TestUtils.waitForCondition(() => { + return !dir.exists(); + }); + + Assert.equal( + dir.exists(), + false, + `Folder ${dir.path} should have been purged by background task` + ); + Assert.equal( + await Glean.networking.residualCacheFolderCount.testGetValue(), + 1 + ); + Assert.equal( + await Glean.networking.residualCacheFolderRemoval.success.testGetValue(), + 1 + ); + Assert.equal( + await Glean.networking.residualCacheFolderRemoval.failure.testGetValue(), + null + ); + + // Check that telemetry properly detects folders failing to be deleted when readonly + // Making folders readonly only works on windows + if (AppConstants.platform == "win") { + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + dir.QueryInterface(Ci.nsILocalFileWin).readOnly = true; + + Services.obs.notifyObservers(null, "idle-daily"); + + await BrowserTestUtils.waitForCondition(async () => { + return ( + (await Glean.networking.residualCacheFolderRemoval.failure.testGetValue()) == + 1 + ); + }); + + Assert.equal( + await Glean.networking.residualCacheFolderCount.testGetValue(), + 2 + ); + Assert.equal( + await Glean.networking.residualCacheFolderRemoval.success.testGetValue(), + 1 + ); + Assert.equal( + await Glean.networking.residualCacheFolderRemoval.failure.testGetValue(), + 1 + ); + + dir.QueryInterface(Ci.nsILocalFileWin).readOnly = false; + dir.remove(true); + } + + Services.prefs.clearUserPref( + "network.cache.shutdown_purge_in_background_task" + ); + Services.prefs.clearUserPref("privacy.clearOnShutdown.cache"); + Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown"); +}); diff --git a/netwerk/test/browser/browser_resource_navigation.js b/netwerk/test/browser/browser_resource_navigation.js new file mode 100644 index 0000000000..56ec280b83 --- /dev/null +++ b/netwerk/test/browser/browser_resource_navigation.js @@ -0,0 +1,76 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +add_task(async function () { + info("Make sure navigation through links in resource:// pages work"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "resource://gre/" }, + async function (browser) { + // Following a directory link shall properly open the directory (bug 1224046) + await SpecialPowers.spawn(browser, [], function () { + let link = Array.prototype.filter.call( + content.document.getElementsByClassName("dir"), + function (element) { + let name = element.textContent; + // Depending whether resource:// is backed by jar: or file://, + // directories either have a trailing slash or they don't. + if (name.endsWith("/")) { + name = name.slice(0, -1); + } + return name == "components"; + } + )[0]; + // First ensure the link is in the viewport + link.scrollIntoView(); + // Then click on it. + link.click(); + }); + + await BrowserTestUtils.browserLoaded( + browser, + undefined, + "resource://gre/components/" + ); + + // Following the parent link shall properly open the parent (bug 1366180) + await SpecialPowers.spawn(browser, [], function () { + let link = content.document + .getElementById("UI_goUp") + .getElementsByTagName("a")[0]; + // The link should always be high enough in the page to be in the viewport. + link.click(); + }); + + await BrowserTestUtils.browserLoaded( + browser, + undefined, + "resource://gre/" + ); + + // Following a link to a given file shall properly open the file. + await SpecialPowers.spawn(browser, [], function () { + let link = Array.prototype.filter.call( + content.document.getElementsByClassName("file"), + function (element) { + return element.textContent == "greprefs.js"; + } + )[0]; + link.scrollIntoView(); + link.click(); + }); + + await BrowserTestUtils.browserLoaded( + browser, + undefined, + "resource://gre/greprefs.js" + ); + + ok(true, "Got to the end of the test!"); + } + ); +}); diff --git a/netwerk/test/browser/browser_speculative_connection_link_header.js b/netwerk/test/browser/browser_speculative_connection_link_header.js new file mode 100644 index 0000000000..24549d30b0 --- /dev/null +++ b/netwerk/test/browser/browser_speculative_connection_link_header.js @@ -0,0 +1,57 @@ +/* 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/. */ + +Services.prefs.setBoolPref("network.http.debug-observations", true); + +registerCleanupFunction(function () { + Services.prefs.clearUserPref("network.http.debug-observations"); +}); + +// Test steps: +// 1. Load file_link_header.sjs +// 2.`<link rel="preconnect" href="https://localhost">` is in +// file_link_header.sjs, so we will create a speculative connection. +// 3. We use "speculative-connect-request" topic to observe whether the +// speculative connection is attempted. +// 4. Finally, we check if the observed host and partition key are the same and +// as the expected. +add_task(async function test_link_preconnect() { + let requestUrl = `https://example.com/browser/netwerk/test/browser/file_link_header.sjs`; + + let observed = ""; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == "speculative-connect-request") { + Services.obs.removeObserver(observer, "speculative-connect-request"); + observed = aData; + } + }, + }; + Services.obs.addObserver(observer, "speculative-connect-request"); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: requestUrl, + waitForLoad: true, + }, + async function () {} + ); + + // The hash key should be like: + // ".S........[tlsflags0x00000000]localhost:443^partitionKey=%28https%2Cexample.com%29" + + // Extracting "localhost:443" + let hostPortRegex = /\[.*\](.*?)\^/; + let hostPortMatch = hostPortRegex.exec(observed); + let hostPort = hostPortMatch ? hostPortMatch[1] : ""; + // Extracting "%28https%2Cexample.com%29" + let partitionKeyRegex = /\^partitionKey=(.*)$/; + let partitionKeyMatch = partitionKeyRegex.exec(observed); + let partitionKey = partitionKeyMatch ? partitionKeyMatch[1] : ""; + + Assert.equal(hostPort, "localhost:443"); + Assert.equal(partitionKey, "%28https%2Cexample.com%29"); +}); diff --git a/netwerk/test/browser/browser_test_data_channel_observer.js b/netwerk/test/browser/browser_test_data_channel_observer.js new file mode 100644 index 0000000000..e03bbc72e6 --- /dev/null +++ b/netwerk/test/browser/browser_test_data_channel_observer.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,<h1>Test"; + +let created = false; + +add_task(async function test_data_channel_observer() { + setupObserver(); + let tab = await BrowserTestUtils.addTab(gBrowser, TEST_URI); + await BrowserTestUtils.waitForCondition(() => created); + ok(created, "We received observer notification"); + await BrowserTestUtils.removeTab(tab); +}); + +function setupObserver() { + const observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe: function observe(subject, topic) { + switch (topic) { + case "data-channel-opened": + let channelURI = subject.QueryInterface(Ci.nsIChannel).URI.spec; + if (channelURI === TEST_URI) { + Services.obs.removeObserver(observer, "data-channel-opened"); + created = true; + } + break; + } + }, + }; + Services.obs.addObserver(observer, "data-channel-opened"); +} diff --git a/netwerk/test/browser/browser_test_favicon.js b/netwerk/test/browser/browser_test_favicon.js new file mode 100644 index 0000000000..99cc6b0922 --- /dev/null +++ b/netwerk/test/browser/browser_test_favicon.js @@ -0,0 +1,26 @@ +// Tests third party cookie blocking using a favicon loaded from a different +// domain. The cookie should be considered third party. +"use strict"; +add_task(async function () { + const iconUrl = + "http://example.org/browser/netwerk/test/browser/damonbowling.jpg"; + const pageUrl = + "http://example.com/browser/netwerk/test/browser/file_favicon.html"; + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior", 1]], + }); + + let promise = TestUtils.topicObserved("cookie-rejected", subject => { + let uri = subject.QueryInterface(Ci.nsIURI); + return uri.spec == iconUrl; + }); + + // Kick off a page load that will load the favicon. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(tab); + }); + + await promise; + ok(true, "foreign favicon cookie was blocked"); +}); diff --git a/netwerk/test/browser/browser_test_io_activity.js b/netwerk/test/browser/browser_test_io_activity.js new file mode 100644 index 0000000000..1e9cb29b6d --- /dev/null +++ b/netwerk/test/browser/browser_test_io_activity.js @@ -0,0 +1,50 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; +const ROOT_URL = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" +); +const TEST_URL = "about:license"; +const TEST_URL2 = ROOT_URL + "ioactivity.html"; + +var gotSocket = false; +var gotFile = false; +var gotSqlite = false; +var gotEmptyData = false; + +function processResults(results) { + for (let data of results) { + console.log(data.location); + gotEmptyData = data.rx == 0 && data.tx == 0 && !gotEmptyData; + gotSocket = data.location.startsWith("socket://127.0.0.1:") || gotSocket; + gotFile = data.location.endsWith("aboutLicense.css") || gotFile; + gotSqlite = data.location.endsWith("places.sqlite") || gotSqlite; + // check for the write-ahead file as well + gotSqlite = data.location.endsWith("places.sqlite-wal") || gotSqlite; + } +} + +add_task(async function testRequestIOActivity() { + await SpecialPowers.pushPrefEnv({ + set: [["io.activity.enabled", true]], + }); + waitForExplicitFinish(); + Services.obs.notifyObservers(null, "profile-initial-state"); + + await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) { + await BrowserTestUtils.withNewTab(TEST_URL2, async function (browser) { + let results = await ChromeUtils.requestIOActivity(); + processResults(results); + + ok(gotSocket, "A socket was used"); + // test deactivated for now + // ok(gotFile, "A file was used"); + ok(gotSqlite, "A sqlite DB was used"); + ok(!gotEmptyData, "Every I/O event had data"); + }); + }); +}); diff --git a/netwerk/test/browser/browser_test_offline_tab.js b/netwerk/test/browser/browser_test_offline_tab.js new file mode 100644 index 0000000000..bf60b4f462 --- /dev/null +++ b/netwerk/test/browser/browser_test_offline_tab.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_set_tab_offline() { + await BrowserTestUtils.withNewTab("https://example.com", async browser => { + // Set the tab to offline + gBrowser.selectedBrowser.browsingContext.forceOffline = true; + + await SpecialPowers.spawn(browser, [], async () => { + try { + await content.fetch("https://example.com/empty.html"); + ok(false, "Should not load since tab is offline"); + } catch (err) { + is(err.name, "TypeError", "Should fail since tab is offline"); + } + }); + }); +}); + +add_task(async function test_set_tab_online() { + await BrowserTestUtils.withNewTab("https://example.com", async browser => { + // Set the tab to online + gBrowser.selectedBrowser.browsingContext.forceOffline = false; + + await SpecialPowers.spawn(browser, [], async () => { + try { + await content.fetch("https://example.com/empty.html"); + ok(true, "Should load since tab is online"); + } catch (err) { + ok(false, "Should not fail since tab is online"); + } + }); + }); +}); diff --git a/netwerk/test/browser/cookie_filtering_helper.sys.mjs b/netwerk/test/browser/cookie_filtering_helper.sys.mjs new file mode 100644 index 0000000000..ab9d721359 --- /dev/null +++ b/netwerk/test/browser/cookie_filtering_helper.sys.mjs @@ -0,0 +1,166 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// The functions in this file will run in the content process in a test +// scope. +/* eslint-env mozilla/simpletest */ +/* global ContentTaskUtils, content */ + +import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs"; + +const info = console.log; + +export var HTTPS_EXAMPLE_ORG = "https://example.org"; +export var HTTPS_EXAMPLE_COM = "https://example.com"; +export var HTTP_EXAMPLE_COM = "http://example.com"; + +export function browserTestPath(uri) { + return uri + "/browser/netwerk/test/browser/"; +} + +export function waitForAllExpectedTests() { + return ContentTaskUtils.waitForCondition(() => { + return content.testDone === true; + }); +} + +export function cleanupObservers() { + Services.obs.notifyObservers(null, "cookie-content-filter-cleanup"); +} + +export async function preclean_test() { + // enable all cookies for the set-cookie trigger via setCookieStringFromHttp + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false); + Services.prefs.setBoolPref( + "network.cookie.sameSite.noneRequiresSecure", + false + ); + Services.prefs.setBoolPref("network.cookie.sameSite.schemeful", false); + + Services.cookies.removeAll(); +} + +export async function cleanup_test() { + Services.prefs.clearUserPref("network.cookie.cookieBehavior"); + Services.prefs.clearUserPref( + "network.cookieJarSettings.unblocked_for_testing" + ); + + Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault"); + Services.prefs.clearUserPref("network.cookie.sameSite.noneRequiresSecure"); + Services.prefs.clearUserPref("network.cookie.sameSite.schemeful"); + + Services.cookies.removeAll(); +} + +export async function fetchHelper(url, cookie, secure, domain = "") { + let headers = new Headers(); + + headers.append("return-set-cookie", cookie); + + if (!secure) { + headers.append("return-insecure-cookie", cookie); + } + + if (domain != "") { + headers.append("return-cookie-domain", domain); + } + + info("fetching " + url); + await fetch(url, { headers }); +} + +// cookie header strings with multiple name=value pairs delimited by \n +// will trigger multiple "cookie-changed" signals +export function triggerSetCookieFromHttp(uri, cookie, fpd = "", ucd = 0) { + info("about to trigger set-cookie: " + uri + " " + cookie); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + if (fpd != "") { + channel.loadInfo.originAttributes = { firstPartyDomain: fpd }; + } + + if (ucd != 0) { + channel.loadInfo.originAttributes = { userContextId: ucd }; + } + Services.cookies.setCookieStringFromHttp(uri, cookie, channel); +} + +export async function triggerSetCookieFromHttpPrivate(uri, cookie) { + info("about to trigger set-cookie: " + uri + " " + cookie); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIPrivateBrowsingChannel); + channel.loadInfo.originAttributes = { privateBrowsingId: 1 }; + channel.setPrivate(true); + Services.cookies.setCookieStringFromHttp(uri, cookie, channel); +} + +// observer/listener function that will be run on the content processes +// listens and checks for the expected cookies +export function checkExpectedCookies(expected, browserName) { + const COOKIE_FILTER_TEST_MESSAGE = "content-added-cookie"; + const COOKIE_FILTER_TEST_CLEANUP = "cookie-content-filter-cleanup"; + + // Counting the expected number of tests is vital to the integrity of these + // tests due to the fact that this test suite relies on triggering tests + // to occur on multiple content processes. + // As such, test modifications/bugs often lead to silent failures. + // Hence, we count to ensure we didn't break anything + // To reduce risk here, we modularize each test as much as possible to + // increase liklihood that a silent failure will trigger a no-test + // error/warning + content.testDone = false; + let testNumber = 0; + + // setup observer that continues listening/testing + function obs(subject, topic) { + // cleanup trigger recieved -> tear down the observer + if (topic == COOKIE_FILTER_TEST_CLEANUP) { + info("cleaning up: " + browserName); + Services.obs.removeObserver(obs, COOKIE_FILTER_TEST_MESSAGE); + Services.obs.removeObserver(obs, COOKIE_FILTER_TEST_CLEANUP); + return; + } + + // test trigger recv'd -> perform test on cookie contents + if (topic == COOKIE_FILTER_TEST_MESSAGE) { + info("Checking if cookie visible: " + browserName); + let result = content.document.cookie; + let resultStr = + "Result " + + result + + " == expected: " + + expected[testNumber] + + " in " + + browserName; + ok(result == expected[testNumber], resultStr); + testNumber++; + if (testNumber >= expected.length) { + info("finishing browser tests: " + browserName); + content.testDone = true; + } + return; + } + + ok(false, "Didn't handle cookie message properly"); // + } + + info("setting up observers: " + browserName); + Services.obs.addObserver(obs, COOKIE_FILTER_TEST_MESSAGE); + Services.obs.addObserver(obs, COOKIE_FILTER_TEST_CLEANUP); +} diff --git a/netwerk/test/browser/cookie_filtering_resource.sjs b/netwerk/test/browser/cookie_filtering_resource.sjs new file mode 100644 index 0000000000..979d56dc9c --- /dev/null +++ b/netwerk/test/browser/cookie_filtering_resource.sjs @@ -0,0 +1,35 @@ +/* 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"; + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + // configure set-cookie domain + let domain = ""; + if (request.hasHeader("return-cookie-domain")) { + domain = "; Domain=" + request.getHeader("return-cookie-domain"); + } + + // configure set-cookie sameSite + let authStr = "; Secure"; + if (request.hasHeader("return-insecure-cookie")) { + authStr = ""; + } + + // use headers to decide if we have them + if (request.hasHeader("return-set-cookie")) { + response.setHeader( + "Set-Cookie", + request.getHeader("return-set-cookie") + authStr + domain, + false + ); + } + + let body = "<!DOCTYPE html> <html> <body> true </body> </html>"; + response.write(body); +} diff --git a/netwerk/test/browser/cookie_filtering_secure_resource_com.html b/netwerk/test/browser/cookie_filtering_secure_resource_com.html new file mode 100644 index 0000000000..e25a719644 --- /dev/null +++ b/netwerk/test/browser/cookie_filtering_secure_resource_com.html @@ -0,0 +1,6 @@ + <!DOCTYPE html> +<html> +<body> +<img src="https://example.com/browser/netwerk/test/browser/cookie_filtering_square.png" width="100px"> +</body> +</html> diff --git a/netwerk/test/browser/cookie_filtering_secure_resource_com.html^headers^ b/netwerk/test/browser/cookie_filtering_secure_resource_com.html^headers^ new file mode 100644 index 0000000000..2bdf118064 --- /dev/null +++ b/netwerk/test/browser/cookie_filtering_secure_resource_com.html^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-store +Set-Cookie: test-cookie=comhtml diff --git a/netwerk/test/browser/cookie_filtering_secure_resource_org.html b/netwerk/test/browser/cookie_filtering_secure_resource_org.html new file mode 100644 index 0000000000..7221dc370d --- /dev/null +++ b/netwerk/test/browser/cookie_filtering_secure_resource_org.html @@ -0,0 +1,6 @@ + <!DOCTYPE html> +<html> +<body> +<img src="https://example.org/browser/netwerk/test/browser/cookie_filtering_square.png" width="100px"> +</body> +</html> diff --git a/netwerk/test/browser/cookie_filtering_secure_resource_org.html^headers^ b/netwerk/test/browser/cookie_filtering_secure_resource_org.html^headers^ new file mode 100644 index 0000000000..924c150ccc --- /dev/null +++ b/netwerk/test/browser/cookie_filtering_secure_resource_org.html^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-store +Set-Cookie: test-cookie=orghtml diff --git a/netwerk/test/browser/cookie_filtering_square.png b/netwerk/test/browser/cookie_filtering_square.png new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/netwerk/test/browser/cookie_filtering_square.png diff --git a/netwerk/test/browser/cookie_filtering_square.png^headers^ b/netwerk/test/browser/cookie_filtering_square.png^headers^ new file mode 100644 index 0000000000..912856ae4a --- /dev/null +++ b/netwerk/test/browser/cookie_filtering_square.png^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-cache +Set-Cookie: test-cookie=png diff --git a/netwerk/test/browser/damonbowling.jpg b/netwerk/test/browser/damonbowling.jpg Binary files differnew file mode 100644 index 0000000000..8bdb2b6042 --- /dev/null +++ b/netwerk/test/browser/damonbowling.jpg diff --git a/netwerk/test/browser/damonbowling.jpg^headers^ b/netwerk/test/browser/damonbowling.jpg^headers^ new file mode 100644 index 0000000000..77f4f49089 --- /dev/null +++ b/netwerk/test/browser/damonbowling.jpg^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-store +Set-Cookie: damon=bowling diff --git a/netwerk/test/browser/dummy.html b/netwerk/test/browser/dummy.html new file mode 100644 index 0000000000..8025fcdb20 --- /dev/null +++ b/netwerk/test/browser/dummy.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> + +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +</head> + +<html> +<body> + <p>Dummy Page</p> +</body> +</html> diff --git a/netwerk/test/browser/early_hint_asset.sjs b/netwerk/test/browser/early_hint_asset.sjs new file mode 100644 index 0000000000..ba52e757ff --- /dev/null +++ b/netwerk/test/browser/early_hint_asset.sjs @@ -0,0 +1,50 @@ +"use strict"; + +function handleRequest(request, response) { + let hinted = + request.hasHeader("X-Moz") && request.getHeader("X-Moz") === "early hint"; + let count = JSON.parse(getSharedState("earlyHintCount")); + if (hinted) { + count.hinted += 1; + } else { + count.normal += 1; + } + setSharedState("earlyHintCount", JSON.stringify(count)); + + let content = ""; + let qs = new URLSearchParams(request.queryString); + let asset = qs.get("as"); + + if (qs.get("cached") === "1") { + response.setHeader("Cache-Control", "max-age=604800", false); + } else { + response.setHeader("Cache-Control", "no-cache", false); + } + + if (asset === "image") { + response.setHeader("Content-Type", "image/png", false); + // set to green/black horizontal stripes (71 bytes) + content = atob( + hinted + ? "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAADklEQVQIW2OU+i/FAAcADoABNV8XGBMAAAAASUVORK5CYII=" + : "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAE0lEQVQIW2P4//+/N8MkBiAGsgA1bAe1SzDY8gAAAABJRU5ErkJggg==" + ); + } else if (asset === "style") { + response.setHeader("Content-Type", "text/css", false); + // green background on hint response, purple response otherwise + content = `#square { background: ${hinted ? "#1aff1a" : "#4b0092"}`; + } else if (asset === "script") { + response.setHeader("Content-Type", "application/javascript", false); + // green background on hint response, purple response otherwise + content = `window.onload = function() { + document.getElementById('square').style.background = "${ + hinted ? "#1aff1a" : "#4b0092" + }"; + }`; + } else if (asset === "fetch") { + response.setHeader("Content-Type", "text/plain", false); + content = hinted ? "hinted" : "normal"; + } + + response.write(content); +} diff --git a/netwerk/test/browser/early_hint_asset_html.sjs b/netwerk/test/browser/early_hint_asset_html.sjs new file mode 100644 index 0000000000..eb5156d4f8 --- /dev/null +++ b/netwerk/test/browser/early_hint_asset_html.sjs @@ -0,0 +1,135 @@ +"use strict"; + +function handleRequest(request, response) { + let qs = new URLSearchParams(request.queryString); + let asset = qs.get("as"); + let hinted = qs.get("hinted") === "1"; + let httpCode = qs.get("code"); + let uuid = qs.get("uuid"); + let cached = qs.get("cached") === "1"; + + let url = `early_hint_asset.sjs?as=${asset}${uuid ? `&uuid=${uuid}` : ""}${ + cached ? "&cached=1" : "" + }`; + + // write to raw socket + response.seizePower(); + let link = ""; + if (hinted) { + response.write("HTTP/1.1 103 Early Hint\r\n"); + if (asset === "fetch" || asset === "font") { + // fetch and font has to specify the crossorigin attribute + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-as + link = `Link: <${url}>; rel=preload; as=${asset}; crossorigin=anonymous\r\n`; + response.write(link); + } else if (asset === "module") { + // module preloads are handled differently + link = `Link: <${url}>; rel=modulepreload\r\n`; + response.write(link); + } else { + link = `Link: <${url}>; rel=preload; as=${asset}\r\n`; + response.write(link); + } + response.write("\r\n"); + } + + let body = ""; + if (asset === "image") { + body = `<!DOCTYPE html> + <html> + <body> + <img src="${url}" width="100px"> + </body> + </html>`; + } else if (asset === "style") { + body = `<!DOCTYPE html> + <html> + <head> + <link rel="stylesheet" type="text/css" href="${url}"> + </head> + <body> + <h1>Test preload css<h1> + <div id="square" style="width:100px;height:100px;"> + </body> + </html> + `; + } else if (asset === "script") { + body = `<!DOCTYPE html> + <html> + <head> + <script src="${url}"></script> + </head> + <body> + <h1>Test preload javascript<h1> + <div id="square" style="width:100px;height:100px;"> + </body> + </html> + `; + } else if (asset === "module") { + // this code assumes that the .sjs for the module is in the same directory + var file_name = url.split("/"); + file_name = file_name[file_name.length - 1]; + body = `<!DOCTYPE html> + <html> + <head> + </head> + <body> + <h1>Test preload module<h1> + <div id="square" style="width:100px;height:100px;"> + <script type="module"> + import { draw } from "./${file_name}"; + draw(); + </script> + </body> + </html> + `; + } else if (asset === "fetch") { + body = `<!DOCTYPE html> + <html> + <body onload="onLoad()"> + <script> + function onLoad() { + fetch("${url}") + .then(r => r.text()) + .then(r => document.getElementsByTagName("h2")[0].textContent = r); + } + </script> + <h1>Test preload fetch</h1> + <h2>Fetching...</h2> + </body> + </html> + `; + } else if (asset === "font") { + body = `<!DOCTYPE html> + <html> + <head> + <style> + @font-face { + font-family: "preloadFont"; + src: url("${url}"); + } + body { + font-family: "preloadFont"; + } + </style> + </head> + <body> + <h1>Test preload font<h1> + </body> + </html> + `; + } + + if (!httpCode) { + response.write(`HTTP/1.1 200 OK\r\n`); + } else { + response.write(`HTTP/1.1 ${httpCode} Error\r\n`); + } + response.write(link); + response.write("Content-Type: text/html;charset=utf-8\r\n"); + response.write("Cache-Control: no-cache\r\n"); + response.write(`Content-Length: ${body.length}\r\n`); + response.write("\r\n"); + response.write(body); + response.finish(); +} diff --git a/netwerk/test/browser/early_hint_csp_options_html.sjs b/netwerk/test/browser/early_hint_csp_options_html.sjs new file mode 100644 index 0000000000..73a6a539d8 --- /dev/null +++ b/netwerk/test/browser/early_hint_csp_options_html.sjs @@ -0,0 +1,120 @@ +"use strict"; + +function handleRequest(request, response) { + let qs = new URLSearchParams(request.queryString); + let asset = qs.get("as"); + let hinted = qs.get("hinted") !== "0"; + let httpCode = qs.get("code"); + let csp = qs.get("csp"); + let csp_in_early_hint = qs.get("csp_in_early_hint"); + let host = qs.get("host"); + + // eslint-disable-next-line mozilla/use-services + let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService( + Ci.nsIUUIDGenerator + ); + let uuid = uuidGenerator.generateUUID().toString(); + let url = `early_hint_pixel.sjs?as=${asset}&uuid=${uuid}`; + if (host) { + url = host + url; + } + + // write to raw socket + response.seizePower(); + + if (hinted) { + response.write("HTTP/1.1 103 Early Hint\r\n"); + if (csp_in_early_hint) { + response.write( + `Content-Security-Policy: ${csp_in_early_hint.replaceAll('"', "")}\r\n` + ); + } + response.write(`Link: <${url}>; rel=preload; as=${asset}\r\n`); + response.write("\r\n"); + } + + let body = ""; + if (asset === "image") { + body = `<!DOCTYPE html> + <html> + <body> + <img id="test_image" src="${url}" width="100px"> + </body> + </html>`; + } else if (asset === "style") { + body = `<!DOCTYPE html> + <html> + <head> + <link rel="stylesheet" type="text/css" href="${url}"> + </head> + <body> + <h1>Test preload css<h1> + <div id="square" style="width:100px;height:100px;"> + </body> + </html> + `; + } else if (asset === "script") { + body = `<!DOCTYPE html> + <html> + <head> + <script src="${url}"></script> + </head> + <body> + <h1>Test preload javascript<h1> + <div id="square" style="width:100px;height:100px;"> + </body> + </html> + `; + } else if (asset === "fetch") { + body = `<!DOCTYPE html> + <html> + <body onload="onLoad()"> + <script> + function onLoad() { + fetch("${url}") + .then(r => r.text()) + .then(r => document.getElementsByTagName("h2")[0].textContent = r); + } + </script> + <h1>Test preload fetch</h1> + <h2>Fetching...</h2> + </body> + </html> + `; + } else if (asset === "font") { + body = `<!DOCTYPE html> + <html> + <head> + <style> + @font-face { + font-family: "preloadFont"; + src: url("${url}") format("woff"); + } + body { + font-family: "preloadFont"; + } + </style> + </head> + <body> + <h1>Test preload font<h1> + </body> + </html> + `; + } + + if (!httpCode) { + response.write(`HTTP/1.1 200 OK\r\n`); + } else { + response.write(`HTTP/1.1 ${httpCode} Error\r\n`); + } + response.write("Content-Type: text/html;charset=utf-8\r\n"); + response.write("Cache-Control: no-cache\r\n"); + response.write(`Content-Length: ${body.length}\r\n`); + if (csp) { + response.write(`Content-Security-Policy: ${csp.replaceAll('"', "")}\r\n`); + } + response.write("\r\n"); + response.write(body); + + response.finish(); +} diff --git a/netwerk/test/browser/early_hint_error.sjs b/netwerk/test/browser/early_hint_error.sjs new file mode 100644 index 0000000000..d3d0da4bd5 --- /dev/null +++ b/netwerk/test/browser/early_hint_error.sjs @@ -0,0 +1,35 @@ +"use strict"; + +function handleRequest(request, response) { + response.setStatusLine( + request.httpVersion, + parseInt(request.queryString), + "Dynamic error" + ); + response.setHeader("Content-Type", "image/png", false); + response.setHeader("Cache-Control", "max-age=604800", false); + + // count requests + let image; + let count = JSON.parse(getSharedState("earlyHintCount")); + if ( + request.hasHeader("X-Moz") && + request.getHeader("X-Moz") === "early hint" + ) { + count.hinted += 1; + // set to green/black horizontal stripes (71 bytes) + image = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAADklEQVQIW2OU+i/FAAcADoABNV8X" + + "GBMAAAAASUVORK5CYII=" + ); + } else { + count.normal += 1; + // set to purple/white checkered pattern (76 bytes) + image = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAE0lEQVQIW2P4//+/N8MkBiAGsgA1" + + "bAe1SzDY8gAAAABJRU5ErkJggg==" + ); + } + setSharedState("earlyHintCount", JSON.stringify(count)); + response.write(image); +} diff --git a/netwerk/test/browser/early_hint_main_html.sjs b/netwerk/test/browser/early_hint_main_html.sjs new file mode 100644 index 0000000000..dc95273c9d --- /dev/null +++ b/netwerk/test/browser/early_hint_main_html.sjs @@ -0,0 +1,62 @@ +"use strict"; + +function handleRequest(request, response) { + // write to raw socket + response.seizePower(); + + let qs = new URLSearchParams(request.queryString); + let imgs = []; + let new_hint = true; + let new_header = true; + for (const [imgUrl, uuid] of qs.entries()) { + if (new_hint) { + // we need to write a new header + new_hint = false; + response.write("HTTP/1.1 103 Early Hint\r\n"); + } + if (!imgUrl.length) { + // next hint in new early hint response when empty string is passed + new_header = true; + if (uuid === "new_response") { + new_hint = true; + response.write("\r\n"); + } else if (uuid === "non_link_header") { + response.write("Content-Length: 25\r\n"); + } + response.write("\r\n"); + } else { + // either append link in new header or in same header + if (new_header) { + new_header = false; + response.write("Link: "); + } else { + response.write(", "); + } + // add query string to make request unique this has the drawback that + // the preloaded image can't accept query strings on it's own / or has + // to strip the appended "?uuid" from the query string before parsing + imgs.push(`<img src="${imgUrl}?${uuid}" width="100px">`); + response.write(`<${imgUrl}?${uuid}>; rel=preload; as=image`); + } + } + if (!new_hint) { + // add separator to main document + response.write("\r\n\r\n"); + } + + let body = `<!DOCTYPE html> +<html> +<body> +${imgs.join("\n")} +</body> +</html>`; + + // main document response + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/html;charset=utf-8\r\n"); + response.write("Cache-Control: no-cache\r\n"); + response.write(`Content-Length: ${body.length}\r\n`); + response.write("\r\n"); + response.write(body); + response.finish(); +} diff --git a/netwerk/test/browser/early_hint_main_redirect.sjs b/netwerk/test/browser/early_hint_main_redirect.sjs new file mode 100644 index 0000000000..0da69d5cee --- /dev/null +++ b/netwerk/test/browser/early_hint_main_redirect.sjs @@ -0,0 +1,67 @@ +"use strict"; + +// In an SJS file we need to get the setTimeout bits ourselves, despite +// what eslint might think applies for browser tests. +// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix +let { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +async function handleRequest(request, response) { + let hinted = + request.hasHeader("X-Moz") && request.getHeader("X-Moz") === "early hint"; + let count = JSON.parse(getSharedState("earlyHintCount")); + if (hinted) { + count.hinted += 1; + } else { + count.normal += 1; + } + setSharedState("earlyHintCount", JSON.stringify(count)); + response.setHeader("Cache-Control", "max-age=604800", false); + + let content = ""; + let qs = new URLSearchParams(request.queryString); + let asset = qs.get("as"); + + if (asset === "image") { + response.setHeader("Content-Type", "image/png", false); + // set to green/black horizontal stripes (71 bytes) + content = atob( + hinted + ? "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAADklEQVQIW2OU+i/FAAcADoABNV8XGBMAAAAASUVORK5CYII=" + : "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAE0lEQVQIW2P4//+/N8MkBiAGsgA1bAe1SzDY8gAAAABJRU5ErkJggg==" + ); + } else if (asset === "style") { + response.setHeader("Content-Type", "text/css", false); + // green background on hint response, purple response otherwise + content = `#square { background: ${hinted ? "#1aff1a" : "#4b0092"}`; + } else if (asset === "script") { + response.setHeader("Content-Type", "application/javascript", false); + // green background on hint response, purple response otherwise + content = `window.onload = function() { + document.getElementById('square').style.background = "${ + hinted ? "#1aff1a" : "#4b0092" + }"; + }`; + } else if (asset === "module") { + response.setHeader("Content-Type", "application/javascript", false); + // green background on hint response, purple response otherwise + content = `export function draw() { + document.getElementById('square').style.background = "${ + hinted ? "#1aff1a" : "#4b0092" + }"; + }`; + } else if (asset === "fetch") { + response.setHeader("Content-Type", "text/plain", false); + content = hinted ? "hinted" : "normal"; + } else if (asset === "font") { + response.setHeader("Content-Type", "font/svg+xml", false); + content = '<font><font-face font-family="preloadFont" /></font>'; + } + response.processAsync(); + setTimeout(() => { + response.write(content); + response.finish(); + }, 0); + //response.write(content); +} diff --git a/netwerk/test/browser/early_hint_pixel.sjs b/netwerk/test/browser/early_hint_pixel.sjs new file mode 100644 index 0000000000..56a64e9af2 --- /dev/null +++ b/netwerk/test/browser/early_hint_pixel.sjs @@ -0,0 +1,37 @@ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Content-Type", "image/png", false); + response.setHeader("Cache-Control", "max-age=604800", false); + + // the typo in "Referer" is part of the http spec + if (request.hasHeader("Referer")) { + setSharedState("requestReferrer", request.getHeader("Referer")); + } else { + setSharedState("requestReferrer", ""); + } + + let count = JSON.parse(getSharedState("earlyHintCount")); + let image; + // send different sized images depending whether this is an early hint request + if ( + request.hasHeader("X-Moz") && + request.getHeader("X-Moz") === "early hint" + ) { + count.hinted += 1; + // set to green/black horizontal stripes (71 bytes) + image = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAADklEQVQIW2OU+i/FAAcADoABNV8X" + + "GBMAAAAASUVORK5CYII=" + ); + } else { + count.normal += 1; + // set to purple/white checkered pattern (76 bytes) + image = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAE0lEQVQIW2P4//+/N8MkBiAGsgA1" + + "bAe1SzDY8gAAAABJRU5ErkJggg==" + ); + } + setSharedState("earlyHintCount", JSON.stringify(count)); + response.write(image); +} diff --git a/netwerk/test/browser/early_hint_pixel_count.sjs b/netwerk/test/browser/early_hint_pixel_count.sjs new file mode 100644 index 0000000000..b59dd035de --- /dev/null +++ b/netwerk/test/browser/early_hint_pixel_count.sjs @@ -0,0 +1,9 @@ +"use strict"; + +function handleRequest(request, response) { + if (request.hasHeader("X-Early-Hint-Count-Start")) { + setSharedState("earlyHintCount", JSON.stringify({ hinted: 0, normal: 0 })); + } + response.setHeader("Content-Type", "application/json", false); + response.write(getSharedState("earlyHintCount")); +} diff --git a/netwerk/test/browser/early_hint_preconnect_html.sjs b/netwerk/test/browser/early_hint_preconnect_html.sjs new file mode 100644 index 0000000000..044c842142 --- /dev/null +++ b/netwerk/test/browser/early_hint_preconnect_html.sjs @@ -0,0 +1,32 @@ +"use strict"; + +function handleRequest(request, response) { + let qs = new URLSearchParams(request.queryString); + let href = qs.get("href"); + let crossOrigin = qs.get("crossOrigin"); + + // write to raw socket + response.seizePower(); + + response.write("HTTP/1.1 103 Early Hint\r\n"); + response.write( + `Link: <${href}>; rel=preconnect; crossOrigin=${crossOrigin}\r\n` + ); + response.write("\r\n"); + + let body = `<!DOCTYPE html> + <html> + <body> + <h1>Test rel=preconnect<h1> + </body> + </html>`; + + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/html;charset=utf-8\r\n"); + response.write("Cache-Control: no-cache\r\n"); + response.write(`Content-Length: ${body.length}\r\n`); + response.write("\r\n"); + response.write(body); + + response.finish(); +} diff --git a/netwerk/test/browser/early_hint_preload_test_helper.sys.mjs b/netwerk/test/browser/early_hint_preload_test_helper.sys.mjs new file mode 100644 index 0000000000..afe6a6bb70 --- /dev/null +++ b/netwerk/test/browser/early_hint_preload_test_helper.sys.mjs @@ -0,0 +1,159 @@ +/* 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/. */ + +import { Assert } from "resource://testing-common/Assert.sys.mjs"; +import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs"; + +const { gBrowser } = Services.wm.getMostRecentWindow("navigator:browser"); + +export async function request_count_checking(testName, got, expected) { + // stringify to pretty print assert output + let g = JSON.stringify(got); + let e = JSON.stringify(expected); + // each early hint request can starts one hinted request, but doesn't yet + // complete the early hint request during the test case + Assert.ok( + got.hinted == expected.hinted, + `${testName}: unexpected amount of hinted request made expected ${expected.hinted} (${e}), got ${got.hinted} (${g})` + ); + // when the early hint request doesn't complete fast enough, another request + // is currently sent from the main document + let expected_normal = expected.normal; + Assert.ok( + got.normal == expected_normal, + `${testName}: unexpected amount of normal request made expected ${expected_normal} (${e}), got ${got.normal} (${g})` + ); +} + +export async function test_hint_preload( + testName, + requestFrom, + imgUrl, + expectedRequestCount, + uuid = undefined +) { + // generate a uuid if none were passed + if (uuid == undefined) { + uuid = Services.uuid.generateUUID(); + } + await test_hint_preload_internal( + testName, + requestFrom, + [[imgUrl, uuid.toString()]], + expectedRequestCount + ); +} + +// - testName is just there to be printed during Asserts when failing +// - the baseUrl can't have query strings, because they are currently used to pass +// the early hint the server responds with +// - urls are in the form [[url1, uuid1], ...]. The uuids are there to make each preload +// unique and not available in the cache from other test cases +// - expectedRequestCount is the sum of all requested objects { normal: count, hinted: count } +export async function test_hint_preload_internal( + testName, + requestFrom, + imgUrls, + expectedRequestCount +) { + // reset the count + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "http://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + let requestUrl = + requestFrom + + "/browser/netwerk/test/browser/early_hint_main_html.sjs?" + + new URLSearchParams(imgUrls).toString(); // encode the hinted images as query string + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: requestUrl, + waitForLoad: true, + }, + async function () {} + ); + + let gotRequestCount = await fetch( + "http://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs" + ).then(response => response.json()); + + await request_count_checking(testName, gotRequestCount, expectedRequestCount); +} + +// Verify that CSP policies in both the 103 response as well as the main response are respected. +// e.g. +// 103 Early Hint +// Content-Security-Policy: style-src: self; +// Link: </style.css>; rel=preload; as=style +// 200 OK +// Content-Security-Policy: style-src: none; +// Link: </font.ttf>; rel=preload; as=font + +// Server-side we verify that: +// - the hinted preload request was made as expected +// - the load request request was made as expected +// Client-side, we verify that the image was loaded or not loaded, depending on the scenario + +// This verifies preload hints and requests +export async function test_preload_hint_and_request(input, expected_results) { + // reset the count + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + let requestUrl = `https://example.com/browser/netwerk/test/browser/early_hint_csp_options_html.sjs?as=${ + input.resource_type + }&hinted=${input.hinted ? "1" : "0"}${input.csp ? "&csp=" + input.csp : ""}${ + input.csp_in_early_hint + ? "&csp_in_early_hint=" + input.csp_in_early_hint + : "" + }${input.host ? "&host=" + input.host : ""}`; + + await BrowserTestUtils.openNewForegroundTab(gBrowser, requestUrl, true); + + let gotRequestCount = await fetch( + "https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs" + ).then(response => response.json()); + + await Assert.deepEqual(gotRequestCount, expected_results, input.test_name); + + gBrowser.removeCurrentTab(); + Services.cache2.clear(); +} + +// simple loading of one url and then checking the request count against the +// passed expected count +export async function test_preload_url(testName, url, expectedRequestCount) { + // reset the count + let headers = new Headers(); + headers.append("X-Early-Hint-Count-Start", ""); + await fetch( + "http://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs", + { headers } + ); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url, + waitForLoad: true, + }, + async function () {} + ); + + let gotRequestCount = await fetch( + "http://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs" + ).then(response => response.json()); + + await request_count_checking(testName, gotRequestCount, expectedRequestCount); + Services.cache2.clear(); +} diff --git a/netwerk/test/browser/early_hint_redirect.sjs b/netwerk/test/browser/early_hint_redirect.sjs new file mode 100644 index 0000000000..6bcb6bdc86 --- /dev/null +++ b/netwerk/test/browser/early_hint_redirect.sjs @@ -0,0 +1,21 @@ +"use strict"; + +function handleRequest(request, response) { + // increase count + let count = JSON.parse(getSharedState("earlyHintCount")); + if ( + request.hasHeader("X-Moz") && + request.getHeader("X-Moz") === "early hint" + ) { + count.hinted += 1; + } else { + count.normal += 1; + } + setSharedState("earlyHintCount", JSON.stringify(count)); + + // respond with redirect + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + let location = request.queryString; + response.setHeader("Location", location, false); + response.write("Hello world!"); +} diff --git a/netwerk/test/browser/early_hint_redirect_html.sjs b/netwerk/test/browser/early_hint_redirect_html.sjs new file mode 100644 index 0000000000..c111af0978 --- /dev/null +++ b/netwerk/test/browser/early_hint_redirect_html.sjs @@ -0,0 +1,24 @@ +"use strict"; + +// usage via url parameters: +// - link: if set sends a link header with the given link value as an early hint repsonse +// - location: sets destination of 301 response + +function handleRequest(request, response) { + let qs = new URLSearchParams(request.queryString); + let link = qs.get("link"); + let location = qs.get("location"); + + // write to raw socket + response.seizePower(); + if (link != undefined) { + response.write("HTTP/1.1 103 Early Hint\r\n"); + response.write(`Link: ${link}\r\n`); + response.write("\r\n"); + } + + response.write("HTTP/1.1 307 Temporary Redirect\r\n"); + response.write(`Location: ${location}\r\n`); + response.write("\r\n"); + response.finish(); +} diff --git a/netwerk/test/browser/early_hint_referrer_policy_html.sjs b/netwerk/test/browser/early_hint_referrer_policy_html.sjs new file mode 100644 index 0000000000..3c8a626de1 --- /dev/null +++ b/netwerk/test/browser/early_hint_referrer_policy_html.sjs @@ -0,0 +1,132 @@ +"use strict"; + +function handleRequest(request, response) { + let qs = new URLSearchParams(request.queryString); + let asset = qs.get("as"); + var action = qs.get("action"); + let hinted = qs.get("hinted") !== "0"; + let httpCode = qs.get("code"); + let header_referrer_policy = qs.get("header_referrer_policy"); + let link_referrer_policy = qs.get("link_referrer_policy"); + + // eslint-disable-next-line mozilla/use-services + let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService( + Ci.nsIUUIDGenerator + ); + let uuid = uuidGenerator.generateUUID().toString(); + let url = `early_hint_pixel.sjs?as=${asset}&uuid=${uuid}`; + + if (action === "get_request_referrer_results") { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + response.write(getSharedState("requestReferrer")); + return; + } else if (action === "reset_referrer_results") { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + response.write(setSharedState("requestReferrer", "not set")); + return; + } + + // write to raw socket + response.seizePower(); + + if (hinted) { + response.write("HTTP/1.1 103 Early Hint\r\n"); + + if (header_referrer_policy) { + response.write( + `Referrer-Policy: ${header_referrer_policy.replaceAll('"', "")}\r\n` + ); + } + + response.write( + `Link: <${url}>; rel=preload; as=${asset}; ${ + link_referrer_policy ? "referrerpolicy=" + link_referrer_policy : "" + } \r\n` + ); + response.write("\r\n"); + } + + let body = ""; + if (asset === "image") { + body = `<!DOCTYPE html> + <html> + <body> + <img src="${url}" width="100px"> + </body> + </html>`; + } else if (asset === "style") { + body = `<!DOCTYPE html> + <html> + <head> + <link rel="stylesheet" type="text/css" href="${url}"> + </head> + <body> + <h1>Test preload css<h1> + <div id="square" style="width:100px;height:100px;"> + </body> + </html> + `; + } else if (asset === "script") { + body = `<!DOCTYPE html> + <html> + <head> + <script src="${url}"></script> + </head> + <body> + <h1>Test preload javascript<h1> + <div id="square" style="width:100px;height:100px;"> + </body> + </html> + `; + } else if (asset === "fetch") { + body = `<!DOCTYPE html> + <html> + <body onload="onLoad()"> + <script> + function onLoad() { + fetch("${url}") + .then(r => r.text()) + .then(r => document.getElementsByTagName("h2")[0].textContent = r); + } + </script> + <h1>Test preload fetch</h1> + <h2>Fetching...</h2> + </body> + </html> + `; + } else if (asset === "font") { + body = `<!DOCTYPE html> + <html> + <head> + <style> + @font-face { + font-family: "preloadFont"; + src: url("${url}") format("woff"); + } + body { + font-family: "preloadFont"; + } + </style> + </head> + <body> + <h1>Test preload font<h1> + </body> + </html> + `; + } + + if (!httpCode) { + response.write(`HTTP/1.1 200 OK\r\n`); + } else { + response.write(`HTTP/1.1 ${httpCode} Error\r\n`); + } + response.write("Content-Type: text/html;charset=utf-8\r\n"); + response.write("Cache-Control: no-cache\r\n"); + response.write(`Content-Length: ${body.length}\r\n`); + response.write("\r\n"); + response.write(body); + + response.finish(); +} diff --git a/netwerk/test/browser/file_favicon.html b/netwerk/test/browser/file_favicon.html new file mode 100644 index 0000000000..77532a3a53 --- /dev/null +++ b/netwerk/test/browser/file_favicon.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> + +<html> + <head> + <link rel="shortcut icon" href="http://example.org/browser/netwerk/test/browser/damonbowling.jpg"> + </head> +</html> diff --git a/netwerk/test/browser/file_link_header.sjs b/netwerk/test/browser/file_link_header.sjs new file mode 100644 index 0000000000..6bab515d19 --- /dev/null +++ b/netwerk/test/browser/file_link_header.sjs @@ -0,0 +1,24 @@ +"use strict"; + +function handleRequest(request, response) { + // write to raw socket + response.seizePower(); + let body = `<!DOCTYPE html> + <html> + <head> + <link rel="preconnect" href="https://localhost"> + </head> + <body> + <h1>Test rel=preconnect<h1> + </body> + </html>`; + + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/html;charset=utf-8\r\n"); + response.write("Cache-Control: no-cache\r\n"); + response.write(`Content-Length: ${body.length}\r\n`); + response.write("\r\n"); + response.write(body); + + response.finish(); +} diff --git a/netwerk/test/browser/file_lnk.lnk b/netwerk/test/browser/file_lnk.lnk Binary files differnew file mode 100644 index 0000000000..abce7587d2 --- /dev/null +++ b/netwerk/test/browser/file_lnk.lnk diff --git a/netwerk/test/browser/ioactivity.html b/netwerk/test/browser/ioactivity.html new file mode 100644 index 0000000000..5e23f6f117 --- /dev/null +++ b/netwerk/test/browser/ioactivity.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> + +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +</head> + +<html> +<body> + <p>IOActivity Test Page</p> +</body> +</html> diff --git a/netwerk/test/browser/no_103_preload.html b/netwerk/test/browser/no_103_preload.html new file mode 100644 index 0000000000..64f5e79259 --- /dev/null +++ b/netwerk/test/browser/no_103_preload.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +<img src="http://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs" width="100px"> +</body> +</html> diff --git a/netwerk/test/browser/no_103_preload.html^headers^ b/netwerk/test/browser/no_103_preload.html^headers^ new file mode 100644 index 0000000000..9e23c73b7f --- /dev/null +++ b/netwerk/test/browser/no_103_preload.html^headers^ @@ -0,0 +1 @@ +Cache-Control: no-cache diff --git a/netwerk/test/browser/post.html b/netwerk/test/browser/post.html new file mode 100644 index 0000000000..9d238c2b97 --- /dev/null +++ b/netwerk/test/browser/post.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>post file</title> +</head> +<body"> +<form id="form" action="auth_post.sjs" method="post" enctype="multipart/form-data"> +<input type="hidden" id="input_hidden" name="foo" value="bar"> +<input id="input_file" name="test_file" type="file"> +<input type="submit"> +</form> +</body> +</html> diff --git a/netwerk/test/browser/redirect.sjs b/netwerk/test/browser/redirect.sjs new file mode 100644 index 0000000000..09e7d9b1e4 --- /dev/null +++ b/netwerk/test/browser/redirect.sjs @@ -0,0 +1,6 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + let location = request.queryString; + response.setHeader("Location", location, false); + response.write("Hello world!"); +} diff --git a/netwerk/test/browser/res.css b/netwerk/test/browser/res.css new file mode 100644 index 0000000000..eab83656ed --- /dev/null +++ b/netwerk/test/browser/res.css @@ -0,0 +1,4 @@ +/* François was here. */ +#purple-text { + color: purple; +} diff --git a/netwerk/test/browser/res.css^headers^ b/netwerk/test/browser/res.css^headers^ new file mode 100644 index 0000000000..e13897f157 --- /dev/null +++ b/netwerk/test/browser/res.css^headers^ @@ -0,0 +1 @@ +Content-Type: text/css; charset=utf-8 diff --git a/netwerk/test/browser/res.csv b/netwerk/test/browser/res.csv new file mode 100644 index 0000000000..b0246d5964 --- /dev/null +++ b/netwerk/test/browser/res.csv @@ -0,0 +1 @@ +1,2,3 diff --git a/netwerk/test/browser/res.csv^headers^ b/netwerk/test/browser/res.csv^headers^ new file mode 100644 index 0000000000..8d30131059 --- /dev/null +++ b/netwerk/test/browser/res.csv^headers^ @@ -0,0 +1 @@ +Content-Type: text/csv; diff --git a/netwerk/test/browser/res.mp3 b/netwerk/test/browser/res.mp3 Binary files differnew file mode 100644 index 0000000000..bad506cf18 --- /dev/null +++ b/netwerk/test/browser/res.mp3 diff --git a/netwerk/test/browser/res.unknown b/netwerk/test/browser/res.unknown new file mode 100644 index 0000000000..3546645658 --- /dev/null +++ b/netwerk/test/browser/res.unknown @@ -0,0 +1 @@ +unknown diff --git a/netwerk/test/browser/res_206.html b/netwerk/test/browser/res_206.html new file mode 100644 index 0000000000..8025fcdb20 --- /dev/null +++ b/netwerk/test/browser/res_206.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> + +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +</head> + +<html> +<body> + <p>Dummy Page</p> +</body> +</html> diff --git a/netwerk/test/browser/res_206.html^headers^ b/netwerk/test/browser/res_206.html^headers^ new file mode 100644 index 0000000000..5a3e3a24c8 --- /dev/null +++ b/netwerk/test/browser/res_206.html^headers^ @@ -0,0 +1,2 @@ +HTTP 206 +Content-Type: text/html; diff --git a/netwerk/test/browser/res_206.mp3 b/netwerk/test/browser/res_206.mp3 Binary files differnew file mode 100644 index 0000000000..bad506cf18 --- /dev/null +++ b/netwerk/test/browser/res_206.mp3 diff --git a/netwerk/test/browser/res_206.mp3^headers^ b/netwerk/test/browser/res_206.mp3^headers^ new file mode 100644 index 0000000000..6e7e4d23ba --- /dev/null +++ b/netwerk/test/browser/res_206.mp3^headers^ @@ -0,0 +1 @@ +HTTP 206 diff --git a/netwerk/test/browser/res_empty.zip b/netwerk/test/browser/res_empty.zip Binary files differnew file mode 100644 index 0000000000..b613b60c02 --- /dev/null +++ b/netwerk/test/browser/res_empty.zip diff --git a/netwerk/test/browser/res_http_index_format b/netwerk/test/browser/res_http_index_format new file mode 100644 index 0000000000..e42645a762 --- /dev/null +++ b/netwerk/test/browser/res_http_index_format @@ -0,0 +1 @@ +100: This is a sample application/http-index-format directory index listing. diff --git a/netwerk/test/browser/res_http_index_format^headers^ b/netwerk/test/browser/res_http_index_format^headers^ new file mode 100644 index 0000000000..f076e27a72 --- /dev/null +++ b/netwerk/test/browser/res_http_index_format^headers^ @@ -0,0 +1 @@ +Content-Type: application/http-index-format diff --git a/netwerk/test/browser/res_img.png b/netwerk/test/browser/res_img.png Binary files differnew file mode 100644 index 0000000000..94e7eb6db2 --- /dev/null +++ b/netwerk/test/browser/res_img.png diff --git a/netwerk/test/browser/res_img_for_unknown_decoder b/netwerk/test/browser/res_img_for_unknown_decoder Binary files differnew file mode 100644 index 0000000000..74d74fde5a --- /dev/null +++ b/netwerk/test/browser/res_img_for_unknown_decoder diff --git a/netwerk/test/browser/res_img_for_unknown_decoder^headers^ b/netwerk/test/browser/res_img_for_unknown_decoder^headers^ new file mode 100644 index 0000000000..defde38020 --- /dev/null +++ b/netwerk/test/browser/res_img_for_unknown_decoder^headers^ @@ -0,0 +1,2 @@ +Content-Type: +Content-Encoding: gzip diff --git a/netwerk/test/browser/res_img_unknown.png b/netwerk/test/browser/res_img_unknown.png new file mode 100644 index 0000000000..8025fcdb20 --- /dev/null +++ b/netwerk/test/browser/res_img_unknown.png @@ -0,0 +1,11 @@ +<!DOCTYPE html> + +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +</head> + +<html> +<body> + <p>Dummy Page</p> +</body> +</html> diff --git a/netwerk/test/browser/res_invalid_partial.mp3 b/netwerk/test/browser/res_invalid_partial.mp3 Binary files differnew file mode 100644 index 0000000000..bad506cf18 --- /dev/null +++ b/netwerk/test/browser/res_invalid_partial.mp3 diff --git a/netwerk/test/browser/res_invalid_partial.mp3^headers^ b/netwerk/test/browser/res_invalid_partial.mp3^headers^ new file mode 100644 index 0000000000..0213f38e4e --- /dev/null +++ b/netwerk/test/browser/res_invalid_partial.mp3^headers^ @@ -0,0 +1,2 @@ +HTTP 206 +Content-Range: bytes 100-1024/* diff --git a/netwerk/test/browser/res_nosniff.html b/netwerk/test/browser/res_nosniff.html new file mode 100644 index 0000000000..8025fcdb20 --- /dev/null +++ b/netwerk/test/browser/res_nosniff.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> + +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +</head> + +<html> +<body> + <p>Dummy Page</p> +</body> +</html> diff --git a/netwerk/test/browser/res_nosniff.html^headers^ b/netwerk/test/browser/res_nosniff.html^headers^ new file mode 100644 index 0000000000..024cdcf5ab --- /dev/null +++ b/netwerk/test/browser/res_nosniff.html^headers^ @@ -0,0 +1,2 @@ +X-Content-Type-Options: nosniff +Content-Type: text/html; diff --git a/netwerk/test/browser/res_nosniff2.html b/netwerk/test/browser/res_nosniff2.html new file mode 100644 index 0000000000..8025fcdb20 --- /dev/null +++ b/netwerk/test/browser/res_nosniff2.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> + +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +</head> + +<html> +<body> + <p>Dummy Page</p> +</body> +</html> diff --git a/netwerk/test/browser/res_nosniff2.html^headers^ b/netwerk/test/browser/res_nosniff2.html^headers^ new file mode 100644 index 0000000000..e46db01e23 --- /dev/null +++ b/netwerk/test/browser/res_nosniff2.html^headers^ @@ -0,0 +1,2 @@ +X-Content-Type-Options: nosniff +Content-Type: text/test diff --git a/netwerk/test/browser/res_not_200or206.mp3 b/netwerk/test/browser/res_not_200or206.mp3 Binary files differnew file mode 100644 index 0000000000..bad506cf18 --- /dev/null +++ b/netwerk/test/browser/res_not_200or206.mp3 diff --git a/netwerk/test/browser/res_not_200or206.mp3^headers^ b/netwerk/test/browser/res_not_200or206.mp3^headers^ new file mode 100644 index 0000000000..dd0b48aaa0 --- /dev/null +++ b/netwerk/test/browser/res_not_200or206.mp3^headers^ @@ -0,0 +1 @@ +HTTP 226 diff --git a/netwerk/test/browser/res_not_ok.html b/netwerk/test/browser/res_not_ok.html new file mode 100644 index 0000000000..8025fcdb20 --- /dev/null +++ b/netwerk/test/browser/res_not_ok.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> + +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +</head> + +<html> +<body> + <p>Dummy Page</p> +</body> +</html> diff --git a/netwerk/test/browser/res_not_ok.html^headers^ b/netwerk/test/browser/res_not_ok.html^headers^ new file mode 100644 index 0000000000..5d15d79e46 --- /dev/null +++ b/netwerk/test/browser/res_not_ok.html^headers^ @@ -0,0 +1 @@ +HTTP 302 Found diff --git a/netwerk/test/browser/res_object.html b/netwerk/test/browser/res_object.html new file mode 100644 index 0000000000..8097415d17 --- /dev/null +++ b/netwerk/test/browser/res_object.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> + +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +</head> + +<html> +<body> + <p>Dummy Page</p> + <script> + let foo = async () => { + let url = "https://example.com/browser/netwerk/test/browser/res_img.png"; + await fetch(url, { mode: "no-cors" }); + } + foo(); + </script> +</body> +</html> diff --git a/netwerk/test/browser/res_sub_document.html b/netwerk/test/browser/res_sub_document.html new file mode 100644 index 0000000000..8025fcdb20 --- /dev/null +++ b/netwerk/test/browser/res_sub_document.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> + +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +</head> + +<html> +<body> + <p>Dummy Page</p> +</body> +</html> diff --git a/netwerk/test/browser/square.png b/netwerk/test/browser/square.png new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/netwerk/test/browser/square.png diff --git a/netwerk/test/browser/test_1629307.html b/netwerk/test/browser/test_1629307.html new file mode 100644 index 0000000000..01f2a0439e --- /dev/null +++ b/netwerk/test/browser/test_1629307.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> +</head> +<body> + <iframe + src="https://example.org/browser/netwerk/test/browser/x_frame_options.html"></iframe> +</body> +</html> diff --git a/netwerk/test/browser/x_frame_options.html b/netwerk/test/browser/x_frame_options.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/netwerk/test/browser/x_frame_options.html diff --git a/netwerk/test/browser/x_frame_options.html^headers^ b/netwerk/test/browser/x_frame_options.html^headers^ new file mode 100644 index 0000000000..dc4bb949f5 --- /dev/null +++ b/netwerk/test/browser/x_frame_options.html^headers^ @@ -0,0 +1,3 @@ +HTTP 401 UNAUTHORIZED +X-Frame-Options: SAMEORIGIN +WWW-Authenticate: basic realm="login required" |