diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /netwerk/test | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/test')
907 files changed, 130549 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.ini b/netwerk/test/browser/browser.ini new file mode 100644 index 0000000000..16e25fd245 --- /dev/null +++ b/netwerk/test/browser/browser.ini @@ -0,0 +1,146 @@ +[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 + 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_about_cache.js] +[browser_bug1535877.js] +[browser_NetUtil.js] +[browser_child_resource.js] +skip-if = + !crashreporter + os == "win" #Bug 1775761 +[browser_post_file.js] +[browser_nsIFormPOSTActionChannel.js] +skip-if = true # protocol handler and channel does not work in content process +[browser_resource_navigation.js] +[browser_test_io_activity.js] +skip-if = socketprocess_networking +[browser_cookie_sync_across_tabs.js] +[browser_test_favicon.js] +skip-if = (verify && (os == 'linux' || os == 'mac')) +support-files = + damonbowling.jpg + damonbowling.jpg^headers^ + file_favicon.html +[browser_fetch_lnk.js] +run-if = os == "win" +support-files = + file_lnk.lnk +[browser_post_auth.js] +skip-if = socketprocess_networking # Bug 1772209 +[browser_backgroundtask_purgeHTTPCache.js] +skip-if = + os == "android" # MultiInstanceLock doesn't work on Android + os == "mac" # intermittent TV timeouts on Mac +[browser_103_telemetry.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_redirect.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_assets.js] +[browser_103_no_cancel_on_error.js] +support-files = + early_hint_preload_test_helper.sys.mjs +[browser_103_redirect_from_server.js] +[browser_cookie_filtering_basic.js] +[browser_cookie_filtering_insecure.js] +[browser_cookie_filtering_oa.js] +[browser_cookie_filtering_cross_origin.js] +[browser_cookie_filtering_subdomain.js] +[browser_103_user_load.js] +support-files = + early_hint_preload_test_helper.sys.mjs +[browser_103_referrer_policy.js] +support-files = + early_hint_referrer_policy_html.sjs + early_hint_preload_test_helper.sys.mjs +[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_preconnect.js] +[browser_103_cleanup.js] +[browser_bug1629307.js] +[browser_103_private_window.js] +[browser_speculative_connection_link_header.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..c8de25c2ca --- /dev/null +++ b/netwerk/test/browser/browser_103_assets.js @@ -0,0 +1,171 @@ +/* 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 +/* TODO(Bug 1798319): enable this test case +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_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..38bf7bffbe --- /dev/null +++ b/netwerk/test/browser/browser_103_preload.js @@ -0,0 +1,137 @@ +/* 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_hint_preload_internal, + 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); +}); + +// Test that with preload config option disabled, no early hint requests are made +add_task(async function test_103_preload_disabled() { + Services.prefs.setBoolPref("network.preload", 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.clearUserPref("network.preload"); +}); + +// 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..2d6daf9501 --- /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.loadURIString(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..bf0f55fc2e --- /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 = + "http://example.com/browser/netwerk/test/browser/103_preload.html"; +var kTestNo103 = + "http://example.com/browser/netwerk/test/browser/no_103_preload.html"; +var kTest404 = + "http://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..01c4b71ab2 --- /dev/null +++ b/netwerk/test/browser/browser_103_user_load.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"; + +// 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 + +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, + 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..99c6eb88cb --- /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.import("resource://testing-common/httpd.js"); + + 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..de2af5f948 --- /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.loadURIString(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.loadURIString(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_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..93be694f3b --- /dev/null +++ b/netwerk/test/browser/browser_post_auth.js @@ -0,0 +1,62 @@ +"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.loadURIString(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..7ccbd6435d --- /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.loadURIString(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_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_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/cookie_filtering_helper.sys.mjs b/netwerk/test/browser/cookie_filtering_helper.sys.mjs new file mode 100644 index 0000000000..f3db10ffb3 --- /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 */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +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 = "cookie-content-filter-test"; + 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..84a3dba37e --- /dev/null +++ b/netwerk/test/browser/early_hint_asset.sjs @@ -0,0 +1,51 @@ +"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 = ""; + Cu.importGlobalProperties(["URLSearchParams"]); + 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..2ac07ca8ed --- /dev/null +++ b/netwerk/test/browser/early_hint_asset_html.sjs @@ -0,0 +1,136 @@ +"use strict"; + +function handleRequest(request, response) { + Cu.importGlobalProperties(["URLSearchParams"]); + 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..17c286f8ac --- /dev/null +++ b/netwerk/test/browser/early_hint_csp_options_html.sjs @@ -0,0 +1,121 @@ +"use strict"; + +function handleRequest(request, response) { + Cu.importGlobalProperties(["URLSearchParams"]); + 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..4f5e751073 --- /dev/null +++ b/netwerk/test/browser/early_hint_error.sjs @@ -0,0 +1,37 @@ +"use strict"; + +Cu.importGlobalProperties(["URLSearchParams"]); + +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..8867aa8754 --- /dev/null +++ b/netwerk/test/browser/early_hint_main_html.sjs @@ -0,0 +1,64 @@ +"use strict"; + +function handleRequest(request, response) { + Cu.importGlobalProperties(["URLSearchParams"]); + + // 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..607b676dc8 --- /dev/null +++ b/netwerk/test/browser/early_hint_main_redirect.sjs @@ -0,0 +1,68 @@ +"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 = ""; + Cu.importGlobalProperties(["URLSearchParams"]); + 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..02f832d28c --- /dev/null +++ b/netwerk/test/browser/early_hint_preconnect_html.sjs @@ -0,0 +1,33 @@ +"use strict"; + +function handleRequest(request, response) { + Cu.importGlobalProperties(["URLSearchParams"]); + 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..17c0f95f76 --- /dev/null +++ b/netwerk/test/browser/early_hint_preload_test_helper.sys.mjs @@ -0,0 +1,131 @@ +/* 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(); +} 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..2cda0b90f7 --- /dev/null +++ b/netwerk/test/browser/early_hint_redirect_html.sjs @@ -0,0 +1,25 @@ +"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) { + Cu.importGlobalProperties(["URLSearchParams"]); + 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..0fb0c0cbc5 --- /dev/null +++ b/netwerk/test/browser/early_hint_referrer_policy_html.sjs @@ -0,0 +1,133 @@ +"use strict"; + +function handleRequest(request, response) { + Cu.importGlobalProperties(["URLSearchParams"]); + 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_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" diff --git a/netwerk/test/crashtests/1274044-1.html b/netwerk/test/crashtests/1274044-1.html new file mode 100644 index 0000000000..cb88e50bcd --- /dev/null +++ b/netwerk/test/crashtests/1274044-1.html @@ -0,0 +1,7 @@ +<script> + +var u = new URL("http://127.0.0.1:9607/"); +u.protocol = "resource:"; +u.port = ""; + +</script> diff --git a/netwerk/test/crashtests/1334468-1.html b/netwerk/test/crashtests/1334468-1.html new file mode 100644 index 0000000000..3d94d69949 --- /dev/null +++ b/netwerk/test/crashtests/1334468-1.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<!-- +user_pref("privacy.firstparty.isolate", true); +--> +<script> + +let RESTRICTED_CHARS = "\001\002\003\004\005\006\007" + + "\010\011\012\013\014\015\016\017" + + "\020\021\022\023\024\025\026\027" + + "\030\031\032\033\034\035\036\037" + + "/:*?\"<>|\\"; + +function boom() { + for (let c of RESTRICTED_CHARS) { + window.location = 'http://s.s' + c; + } +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/netwerk/test/crashtests/1399467-1.html b/netwerk/test/crashtests/1399467-1.html new file mode 100644 index 0000000000..7d16e119d0 --- /dev/null +++ b/netwerk/test/crashtests/1399467-1.html @@ -0,0 +1 @@ +<script src='ftp:%'> diff --git a/netwerk/test/crashtests/1793521.html b/netwerk/test/crashtests/1793521.html new file mode 100644 index 0000000000..5bd19df01d --- /dev/null +++ b/netwerk/test/crashtests/1793521.html @@ -0,0 +1 @@ +<iframe src='http://൉D-'></iframe> diff --git a/netwerk/test/crashtests/675518.html b/netwerk/test/crashtests/675518.html new file mode 100644 index 0000000000..44a43570e4 --- /dev/null +++ b/netwerk/test/crashtests/675518.html @@ -0,0 +1,21 @@ +<!DOCTYPE html>
+<script>
+function boom()
+{
+ var frame = document.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
+ frame.src = "javascript:'<html><body>1</body></html>';";
+ document.body.appendChild(frame);
+ var frameWin = frame.contentWindow;
+
+ var resizeListener = function() {
+ frameWin.removeEventListener("resize", resizeListener, false);
+ frameWin.document.write("3...");
+ };
+ frameWin.addEventListener("resize", resizeListener, false);
+
+ frameWin.document.write("2...");
+}
+
+</script>
+<body onload="boom();"></body>
+</html>
diff --git a/netwerk/test/crashtests/785753-1.html b/netwerk/test/crashtests/785753-1.html new file mode 100644 index 0000000000..cfcedead02 --- /dev/null +++ b/netwerk/test/crashtests/785753-1.html @@ -0,0 +1,253 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 0.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> +<title>Test for importing styles via incorrect link element</title> +<link type="text/css" href="data:text/css;charset=utf-8,p#one%-32519279132875%7Bbackground-color%32769A%340282366920938463463374607431768211436red%1B%7D%0D%0A"/> +<link rel="stylesheet" href="data:text/css;charset=utf-16,p#two%1%7Bbackground-color%65535A%4294967297lime%3B%7D%0D%0A"/> +</head> +<link href="data:text/css;charset=utf-8,p#three%1%7Bbackground-color%3A%20red%3B%7D%0D%0A"/> +<link type="text/css" rel="stylesheet" href="data:text/css;charset=utf-8,p#four%32767%7Bbackground-color%2147483649A%20lime%257B%7D%0D%0A"/> +</head> +<body> +<p id="one">This line should not have red background</p> +<p id="two">This line should have lime background</p> +<p id="three">This line should not have red background</p> +<p id="four">This line should have lime background</p> +</body> +<script type="text/javascript"> + function alert(msg){}; function confirm(msg){}; function prompt(msg){}; + try{ document.head.appendChild(document.createElement("style"));}catch(e){} + var styleSheet = document.styleSheets[document.styleSheets.length-1]; +try{if(styleSheet.length===undefined){styleSheet.insertRule(":root{}",0); styleSheet.disabled=false} + +styleSheet.insertRule("body {counter-reset:c}",0)}catch(e){} +var styleSheet0 = document.styleSheets[0]; +var styleSheet1 = document.styleSheets[1]; +var styleSheet2 = document.styleSheets[2]; +var test0=document.getElementById("four") +var test1=document.getElementById("one") +var test2=document.getElementById("two") +var test3=document.getElementById("three") +setTimeout(function(){ +try{test0.style['padding-top']='32px';}catch(e){} +try{test2.insertBefore(test3);}catch(e){} +try{test0.style.setProperty('background-image','url(data:image/gif;base64,R0lGODlhEAAOALMAAOazToeHh0tLS/7LZv/0jvb29t/f3//Ub//ge8WSLf/rhf/3kdbW1mxsbP//mf///yH5BAAAAAAALAAAAAAQAA4AAARe8L1Ekyky67QZ1hLnjM5UUde0ECwLJoExKcppV0aCcGCmTIHEIUEqjgaORCMxIC6e0CcguWw6aFjsVMkkIr7g77ZKPJjPZqIyd7sJAgVGoEGv2xsBxqNgYPj/gAwXEQA7)','important');}catch(e){} +try{test3.style['line-height']='-324px';}catch(e){} +try{test0.style.setProperty('border-image-repeat','repeat','important');}catch(e){} +},3); + +setTimeout(function(){ +try{test1.style['line-height']='43px';}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined{clear:right; }",styleSheet0.cssRules.length);}catch(e){} +try{test2.style.setProperty('bottom','inherit','important');}catch(e){} +try{test3.remove()}catch(e){}; +try{test0.style.setProperty('overflow-x','no-content','important');}catch(e){} +},0); + +setTimeout(function(){ +try{styleSheet0.insertRule(".undefined,.undefined{font-size:40px; overflow-x:no-content; -moz-transition-duration:-5.408991568721831s; column-span:483; }",styleSheet0.cssRules.length);}catch(e){} +try{test0.remove()}catch(e){}; +try{test0.insertBefore(test2);}catch(e){} +try{test3.style.setProperty('bottom','inherit','important');}catch(e){} +try{test2.style.setProperty('background-origin','border-box','important');}catch(e){} +},2); + +setTimeout(function(){ +window.resizeTo(1018,353) +try{test0.insertBefore(test1);}catch(e){} +try{test1.style.setProperty('bottom','auto','important');}catch(e){} +try{test1.style.setProperty('z-index','inherit','important');}catch(e){} +window.resizeTo(1018,353) +},1); + +setTimeout(function(){ +try{test0.innerHtml=test3.innerHtml;}catch(e){} +document.body.style.setProperty('-webkit-filter','blur(18px)','null') +try{test3.remove()}catch(e){}; +try{test0.style.setProperty('padding-right','18px','important');}catch(e){} +try{test3.style.setProperty('border-bottom-color','rgb(96%,328%,106)','important');}catch(e){} +},4); + +setTimeout(function(){ +try{test2.innerHtml=test0.innerHtml;}catch(e){} +try{styleSheet1.insertRule(".undefined,.undefined{list-style-type:sidama; background-clip:border-box; overflow-x:scroll; border-bottom-left-radius:70px; text-transform:uppercase; empty-cells:inherit; }",styleSheet1.cssRules.length);}catch(e){} +try{styleSheet1.insertRule(".undefined:active {min-width:759; }",styleSheet1.cssRules.length);}catch(e){} +try{test1.style.setProperty('top','343','important');}catch(e){} +try{test2.replaceChild(test0,test2.firstChild)}catch(e){} +},4); + +setTimeout(function(){ +try{styleSheet1.insertRule(".undefined,.undefined,.undefined,.undefined{background-attachment:inherit; flood-color:rgba(93%,364%,104,4.471563883125782); }",0);}catch(e){} +try{test1.style.setProperty('font-style','oblique','important');}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined,.undefined{border-right-style:double; }",styleSheet0.cssRules.length);}catch(e){} +document.execCommand("SelectAll", true); +try{test3.remove()}catch(e){}; +},1); + +setTimeout(function(){ +try{test1.remove()}catch(e){}; +try{test0.innerHtml=test2.innerHtml;}catch(e){} +try{test1.remove()}catch(e){}; +try{test0.remove()}catch(e){}; +try{test2.innerHtml=test2.innerHtml;}catch(e){} +},4); + +setTimeout(function(){ +try{test0.appendChild(test1);}catch(e){} +try{test1.style['position']='inherit';}catch(e){} +try{test2.replaceChild(test3,test2.lastChild)}catch(e){} +try{test1.style['border-left-color']='#6D8997';}catch(e){} +try{test1.innerHtml=test3.innerHtml;}catch(e){} +},6); + +setTimeout(function(){ +try{test2.insertBefore(test1);}catch(e){} +try{test2.innerHtml=test3.innerHtml;}catch(e){} +try{test0.appendChild(test2);}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined{resize:both; background-color:#A22225; position:relative; column-width:auto; letter-spacing:361px; border-top-width:151%; }",styleSheet0.cssRules.length);}catch(e){} +document.body.style.zoom=1.8764980849809945 +},6); + +setTimeout(function(){ +try{styleSheet1.insertRule(".undefined,.undefined,.undefined{outline-color:rgba(126,179,46,0.8964905887842178); width:183; }",styleSheet1.cssRules.length);}catch(e){} +try{test2.insertBefore(test3);}catch(e){} +try{test1.innerHtml=test3.innerHtml;}catch(e){} +try{styleSheet0.insertRule("article,footer,article,article{border-bottom-right-radius:7px; }",0);}catch(e){} +try{test1.insertBefore(test0);}catch(e){} +},4); + +setTimeout(function(){ +try{test0.remove()}catch(e){}; +try{styleSheet1.insertRule(".undefined,.undefined,.undefined,.undefined{display: table-header-group; content: counter(c, ethiopic); counter-increment:c;}",0);}catch(e){} +try{test0.style.setProperty('background-color','#8897D3','important');}catch(e){} +try{test3.appendChild(test0);}catch(e){} +try{styleSheet0.insertRule("hgroup,hgroup,hgroup{outline-color:rgba(167,242,90%,-0.10827295063063502); }",styleSheet0.cssRules.length);}catch(e){} +},4); + +setTimeout(function(){ +try{test3.style.setProperty('border-bottom-color','#55D7F6','important');}catch(e){} +try{test1.remove()}catch(e){}; +try{test2.insertBefore(test1);}catch(e){} +try{test2.innerHtml=test0.innerHtml;}catch(e){} +try{styleSheet1.insertRule("#one,#one,#three,#three{background-clip:border-box; border-top-width:85em; }",0);}catch(e){} +},5); + +setTimeout(function(){ +try{test3.appendChild(test0);}catch(e){} +try{test2.innerHtml=test1.innerHtml;}catch(e){} +try{test2.style['background-attachment']='inherit';}catch(e){} +try{test3.style['clip']='inherit';}catch(e){} +try{test3.remove()}catch(e){}; +},3); + +setTimeout(function(){ +try{test1.remove()}catch(e){}; +try{styleSheet0.insertRule(".undefined,.undefined{height:424; }",styleSheet0.cssRules.length);}catch(e){} +try{test2.style.setProperty('border-top-style','solid','important');}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined,.undefined,.undefined{page-break-inside:left; border-image-slice:fill; border-left-width:184pc; }",styleSheet0.cssRules.length);}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined{margin-left:-105px; text-transform:inherit; box-sizing:border-box; }",styleSheet0.cssRules.length);}catch(e){} +},4); + +setTimeout(function(){ +try{styleSheet0.insertRule("param,img,img,param{left:auto; background-clip:padding-box; }",styleSheet0.cssRules.length);}catch(e){} +try{test2.style.setProperty('min-height','605','important');}catch(e){} +try{test2.remove()}catch(e){}; +try{styleSheet1.insertRule(".undefined::first-line, #two::first-line {border-bottom-width:69pt; lighting-color:rgba(75%,166,81,-0.8728196211159229); text-shadow:85px 459px #2F1; }",styleSheet1.cssRules.length);}catch(e){} +try{test3.appendChild(document.createTextNode(unescape("!F幓[")))}catch(e){} +},4); + +setTimeout(function(){ +try{styleSheet0.insertRule("#two,#four{font-style:italic; list-style-position:inside; border-collapse:inherit; word-wrap:break-word; text-transform:uppercase; }",styleSheet0.cssRules.length);}catch(e){} +try{test1.style.setProperty('border-bottom-right-radius','2px','important');}catch(e){} +try{test3.style['text-shadow']='58px 64px rgba(61%,60,199,0.03203143551945686)';}catch(e){} +try{styleSheet1.insertRule("dir,dir,nav{display: inline-table; content: counter(c, upper-greek); counter-increment:c;}",styleSheet1.cssRules.length);}catch(e){} +try{styleSheet0.insertRule("#one:target, #three:after {color:#D9B; outline-style:hidden; flood-color:rgba(22,59%,99%,-0.008097740123048425); }",0);}catch(e){} +},6); + +setTimeout(function(){ +try{test2.remove()}catch(e){}; +document.execCommand("Copy", true); +try{test3.style['letter-spacing']='102px';}catch(e){} +try{test2.remove()}catch(e){}; +try{test3.appendChild(test0);}catch(e){} +},5); + +setTimeout(function(){ +try{test0.style['text-transform']='inherit';}catch(e){} +try{test0.style['word-break']='hyphenate';}catch(e){} +try{test3.insertBefore(test2);}catch(e){} +try{test2.style.setProperty('bottom','410','important');}catch(e){} +try{test1.style['background']='url(data:image/gif;base64,R0lGODlhEAAOALMAAOazToeHh0tLS/7LZv/0jvb29t/f3//Ub//ge8WSLf/rhf/3kdbW1mxsbP//mf///yH5BAAAAAAALAAAAAAQAA4AAARe8L1Ekyky67QZ1hLnjM5UUde0ECwLJoExKcppV0aCcGCmTIHEIUEqjgaORCMxIC6e0CcguWw6aFjsVMkkIr7g77ZKPJjPZqIyd7sJAgVGoEGv2xsBxqNgYPj/gAwXEQA7)';}catch(e){} +},5); + +setTimeout(function(){ +try{test2.appendChild(test2);}catch(e){} +try{test1.appendChild(test2);}catch(e){} +try{test2.appendChild(document.createTextNode(unescape("zn!쎔gw눢fb¤£kꄍ£3wa02fnpå0!äwC䰴頥!!a")))}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined,.undefined{display: inline; content: counter(c, khmer); counter-increment:c;}",0);}catch(e){} +try{test3.style.setProperty('line-height','42in','important');}catch(e){} +},7); + +setTimeout(function(){ +window.moveBy(133,126) +try{test0.style['padding-right']='20px';}catch(e){} +try{test1.replaceChild(test2,test1.firstChild)}catch(e){} +try{test1.style.setProperty('letter-spacing','120px','important');}catch(e){} +try{test1.style.setProperty('height','57','important');}catch(e){} +},4); + +setTimeout(function(){ +try{styleSheet1.insertRule(".undefined:nth-child(even), #one:nth-last-child(even) {margin-top:-241cm; font-size:23px; }",0);}catch(e){} +try{styleSheet0.insertRule(".undefined:nth-child(even), #three:default {stop-color:rgba(92%,-201%,31,0.8133529485203326); lighting-color:#E31; }",styleSheet0.cssRules.length);}catch(e){} +try{test1.style.setProperty('border-top-left-radius','63px','important');}catch(e){} +try{test0.style['letter-spacing']='36px';}catch(e){} +try{test1.appendChild(document.createTextNode(unescape("1u£Fⶵ隗(籬fsä⍉㯗cሮ銐k䆴n#蹹圭篺(1w馁")))}catch(e){} +},7); + +setTimeout(function(){ +try{test1.appendChild(test3);}catch(e){} +try{test2.style.setProperty('lighting-color','rgb(4,36%,95%)','important');}catch(e){} +try{test0.style.setProperty('border-right-width','71pt','important');}catch(e){} +try{test2.style['box-shadow']='-228px , 86px , 9px , #F0A134';}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined,.undefined{left:inherit; }",styleSheet0.cssRules.length);}catch(e){} +},7); + +setTimeout(function(){ +try{styleSheet1.insertRule(".undefined,.undefined,.undefined,.undefined{min-height:277; -moz-transition-property:none; }",0);}catch(e){} +try{test0.style.setProperty('word-wrap','break-word','important');}catch(e){} +try{test1.style.setProperty('top','inherit','important');}catch(e){} +try{styleSheet0.insertRule(".undefined,.undefined,.undefined{overflow-x:visible; border-bottom-left-radius:844488.660965659px; }",0);}catch(e){} +try{test2.style.setProperty('margin-bottom','auto','important');}catch(e){} +},0); + +setTimeout(function(){ +try{styleSheet1.insertRule("hgroup,hgroup,dl,dl{display: list-item; content: counter(c, hebrew); counter-increment:c;}",styleSheet1.cssRules.length);}catch(e){} +try{test0.style.setProperty('border-image-repeat','repeat','important');}catch(e){} +try{styleSheet0.insertRule("figure,footer,figure,table{-moz-border-image-repeat:stretch; word-wrap:normal; border-right-color:rgb(4%,19%,6%); caption-side:top; stop-color:rgba(450%,226,14%,1.5385327017866075); }",styleSheet0.cssRules.length);}catch(e){} +try{test3.innerHtml=test2.innerHtml;}catch(e){} +try{test2.appendChild(test0);}catch(e){} +},0); + +setTimeout(function(){ +try{test1.replaceChild(test0,test1.lastChild)}catch(e){} +try{test2.style.setProperty('border-collapse','inherit','important');}catch(e){} +try{test1.style['overflow-x']='visible';}catch(e){} +try{test1.style['text-indent']='-30.283706605434418cm';}catch(e){} +try{styleSheet0.insertRule("tt,hgroup{stroke-width:-439px; box-sizing:border-box; }",styleSheet0.cssRules.length);}catch(e){} +},1); + +setTimeout(function(){ +styleSheet0.disabled=true +styleSheet1.disabled=false +styleSheet1.disabled=true +document.body.style.setProperty('-webkit-filter','invert(338%)','null') +window.moveBy(302,115) +},0); + +setTimeout(function(){ +window.blur() +},4); + +</script> + +</html> diff --git a/netwerk/test/crashtests/785753-2.html b/netwerk/test/crashtests/785753-2.html new file mode 100644 index 0000000000..15a9865388 --- /dev/null +++ b/netwerk/test/crashtests/785753-2.html @@ -0,0 +1,3 @@ +<link rel="stylesheet" href="data:text/css;charset=utf-16,a"/> + +<link rel="stylesheet" href="data:text/css;charset=utf-16,p#two%1%7Bbackground-color%65535A%4294967297lime%3B%7D%0D%0A"/>
\ No newline at end of file diff --git a/netwerk/test/crashtests/crashtests.list b/netwerk/test/crashtests/crashtests.list new file mode 100644 index 0000000000..01ffacd8e3 --- /dev/null +++ b/netwerk/test/crashtests/crashtests.list @@ -0,0 +1,7 @@ +skip load 675518.html +load 785753-1.html +load 785753-2.html +load 1274044-1.html +skip-if(ThreadSanitizer) skip-if(Android) pref(privacy.firstparty.isolate,true) load 1334468-1.html # Bug 1639080 +load 1399467-1.html +load 1793521.html diff --git a/netwerk/test/fuzz/FuzzingStreamListener.cpp b/netwerk/test/fuzz/FuzzingStreamListener.cpp new file mode 100644 index 0000000000..878b116c5b --- /dev/null +++ b/netwerk/test/fuzz/FuzzingStreamListener.cpp @@ -0,0 +1,44 @@ +#include "FuzzingInterface.h" +#include "FuzzingStreamListener.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(FuzzingStreamListener, nsIStreamListener, nsIRequestObserver) + +NS_IMETHODIMP +FuzzingStreamListener::OnStartRequest(nsIRequest* aRequest) { + FUZZING_LOG(("FuzzingStreamListener::OnStartRequest")); + return NS_OK; +} + +NS_IMETHODIMP +FuzzingStreamListener::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + FUZZING_LOG(("FuzzingStreamListener::OnDataAvailable")); + static uint32_t const kCopyChunkSize = 128 * 1024; + uint32_t toRead = std::min<uint32_t>(aCount, kCopyChunkSize); + nsCString data; + + while (aCount) { + nsresult rv = NS_ReadInputStreamToString(aInputStream, data, toRead); + if (NS_FAILED(rv)) { + return rv; + } + aCount -= toRead; + toRead = std::min<uint32_t>(aCount, kCopyChunkSize); + } + return NS_OK; +} + +NS_IMETHODIMP +FuzzingStreamListener::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + FUZZING_LOG(("FuzzingStreamListener::OnStopRequest")); + mChannelDone = true; + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/test/fuzz/FuzzingStreamListener.h b/netwerk/test/fuzz/FuzzingStreamListener.h new file mode 100644 index 0000000000..86f60ed102 --- /dev/null +++ b/netwerk/test/fuzz/FuzzingStreamListener.h @@ -0,0 +1,37 @@ +#ifndef FuzzingStreamListener_h__ +#define FuzzingStreamListener_h__ + +#include "mozilla/SpinEventLoopUntil.h" +#include "nsCOMPtr.h" +#include "nsNetCID.h" +#include "nsString.h" +#include "nsNetUtil.h" +#include "nsIStreamListener.h" + +namespace mozilla { +namespace net { + +class FuzzingStreamListener final : public nsIStreamListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + FuzzingStreamListener() = default; + + void waitUntilDone() { + SpinEventLoopUntil("net::FuzzingStreamListener::waitUntilDone"_ns, + [&]() { return mChannelDone; }); + } + + bool isDone() { return mChannelDone; } + + private: + ~FuzzingStreamListener() = default; + bool mChannelDone = false; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/test/fuzz/TestHttpFuzzing.cpp b/netwerk/test/fuzz/TestHttpFuzzing.cpp new file mode 100644 index 0000000000..e717b608e7 --- /dev/null +++ b/netwerk/test/fuzz/TestHttpFuzzing.cpp @@ -0,0 +1,297 @@ +#include "mozilla/LoadInfo.h" +#include "mozilla/Preferences.h" +#include "mozilla/SpinEventLoopUntil.h" + +#include "nsCOMPtr.h" +#include "nsNetCID.h" +#include "nsString.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "nsILoadInfo.h" +#include "nsIProxiedProtocolHandler.h" +#include "nsIOService.h" +#include "nsProtocolProxyService.h" +#include "nsScriptSecurityManager.h" +#include "nsServiceManagerUtils.h" +#include "nsNetUtil.h" +#include "NullPrincipal.h" +#include "nsCycleCollector.h" +#include "RequestContextService.h" +#include "nsSandboxFlags.h" + +#include "FuzzingInterface.h" +#include "FuzzingStreamListener.h" +#include "FuzzyLayer.h" + +namespace mozilla { +namespace net { + +// Target spec and optional proxy type to use, set by the respective +// initialization function so we can cover all combinations. +static nsAutoCString httpSpec; +static nsAutoCString proxyType; +static size_t minSize; + +static int FuzzingInitNetworkHttp(int* argc, char*** argv) { + Preferences::SetBool("network.dns.native-is-localhost", true); + Preferences::SetBool("fuzzing.necko.enabled", true); + Preferences::SetInt("network.http.speculative-parallel-limit", 0); + Preferences::SetInt("network.http.http2.default-concurrent", 1); + + if (httpSpec.IsEmpty()) { + httpSpec = "http://127.0.0.1/"; + } + + net_EnsurePSMInit(); + + return 0; +} + +static int FuzzingInitNetworkHttp2(int* argc, char*** argv) { + httpSpec = "https://127.0.0.1/"; + return FuzzingInitNetworkHttp(argc, argv); +} + +static int FuzzingInitNetworkHttp3(int* argc, char*** argv) { + Preferences::SetBool("fuzzing.necko.http3", true); + Preferences::SetBool("network.http.http3.enable", true); + Preferences::SetCString("network.http.http3.alt-svc-mapping-for-testing", + "fuzz.bad.tld;h3=:443"); + httpSpec = "https://fuzz.bad.tld/"; + minSize = 1200; + return FuzzingInitNetworkHttp(argc, argv); +} + +static int FuzzingInitNetworkHttpProxyHttp2(int* argc, char*** argv) { + // This is http over an https proxy + proxyType = "https"; + + return FuzzingInitNetworkHttp(argc, argv); +} + +static int FuzzingInitNetworkHttp2ProxyHttp2(int* argc, char*** argv) { + // This is https over an https proxy + proxyType = "https"; + + return FuzzingInitNetworkHttp2(argc, argv); +} + +static int FuzzingInitNetworkHttpProxyPlain(int* argc, char*** argv) { + // This is http over an http proxy + proxyType = "http"; + + return FuzzingInitNetworkHttp(argc, argv); +} + +static int FuzzingInitNetworkHttp2ProxyPlain(int* argc, char*** argv) { + // This is https over an http proxy + proxyType = "http"; + + return FuzzingInitNetworkHttp2(argc, argv); +} + +static int FuzzingRunNetworkHttp(const uint8_t* data, size_t size) { + if (size < minSize) { + return 0; + } + + // Set the data to be processed + addNetworkFuzzingBuffer(data, size); + + nsWeakPtr channelRef; + + nsCOMPtr<nsIRequestContextService> rcsvc = + mozilla::net::RequestContextService::GetOrCreate(); + uint64_t rcID; + + { + nsCOMPtr<nsIURI> url; + nsresult rv; + + if (NS_NewURI(getter_AddRefs(url), httpSpec) != NS_OK) { + MOZ_CRASH("Call to NS_NewURI failed."); + } + + nsLoadFlags loadFlags; + loadFlags = nsIRequest::LOAD_BACKGROUND | nsIRequest::LOAD_BYPASS_CACHE | + nsIRequest::INHIBIT_CACHING | + nsIRequest::LOAD_FRESH_CONNECTION | + nsIChannel::LOAD_INITIAL_DOCUMENT_URI; + nsSecurityFlags secFlags; + secFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; + uint32_t sandboxFlags = SANDBOXED_ORIGIN; + + nsCOMPtr<nsIChannel> channel; + nsCOMPtr<nsILoadInfo> loadInfo; + + if (!proxyType.IsEmpty()) { + nsAutoCString proxyHost("127.0.0.2"); + + nsCOMPtr<nsIProtocolProxyService2> ps = + do_GetService(NS_PROTOCOLPROXYSERVICE_CID); + if (!ps) { + MOZ_CRASH("Failed to create nsIProtocolProxyService2"); + } + + mozilla::net::nsProtocolProxyService* pps = + static_cast<mozilla::net::nsProtocolProxyService*>(ps.get()); + + nsCOMPtr<nsIProxyInfo> proxyInfo; + rv = pps->NewProxyInfo(proxyType, proxyHost, 443, + ""_ns, // aProxyAuthorizationHeader + ""_ns, // aConnectionIsolationKey + 0, // aFlags + UINT32_MAX, // aFailoverTimeout + nullptr, // aFailoverProxy + getter_AddRefs(proxyInfo)); + + if (NS_FAILED(rv)) { + MOZ_CRASH("Call to NewProxyInfo failed."); + } + + nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv); + if (NS_FAILED(rv)) { + MOZ_CRASH("do_GetIOService failed."); + } + + nsCOMPtr<nsIProtocolHandler> handler; + rv = ioService->GetProtocolHandler("http", getter_AddRefs(handler)); + if (NS_FAILED(rv)) { + MOZ_CRASH("GetProtocolHandler failed."); + } + + nsCOMPtr<nsIProxiedProtocolHandler> pph = do_QueryInterface(handler, &rv); + if (NS_FAILED(rv)) { + MOZ_CRASH("do_QueryInterface failed."); + } + + loadInfo = new LoadInfo( + nsContentUtils::GetSystemPrincipal(), // loading principal + nsContentUtils::GetSystemPrincipal(), // triggering principal + nullptr, // Context + secFlags, nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST, + Maybe<mozilla::dom::ClientInfo>(), + Maybe<mozilla::dom::ServiceWorkerDescriptor>(), sandboxFlags); + + rv = pph->NewProxiedChannel(url, proxyInfo, + 0, // aProxyResolveFlags + nullptr, // aProxyURI + loadInfo, getter_AddRefs(channel)); + + if (NS_FAILED(rv)) { + MOZ_CRASH("Call to newProxiedChannel failed."); + } + } else { + rv = NS_NewChannel(getter_AddRefs(channel), url, + nsContentUtils::GetSystemPrincipal(), secFlags, + nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST, + nullptr, // aCookieJarSettings + nullptr, // aPerformanceStorage + nullptr, // loadGroup + nullptr, // aCallbacks + loadFlags, // aLoadFlags + nullptr, // aIoService + sandboxFlags); + + if (NS_FAILED(rv)) { + MOZ_CRASH("Call to NS_NewChannel failed."); + } + + loadInfo = channel->LoadInfo(); + } + + if (NS_FAILED(loadInfo->SetSkipContentSniffing(true))) { + MOZ_CRASH("Failed to call SetSkipContentSniffing"); + } + + RefPtr<FuzzingStreamListener> gStreamListener; + nsCOMPtr<nsIHttpChannel> gHttpChannel; + + gHttpChannel = do_QueryInterface(channel); + rv = gHttpChannel->SetRequestMethod("GET"_ns); + if (NS_FAILED(rv)) { + MOZ_CRASH("SetRequestMethod on gHttpChannel failed."); + } + + nsCOMPtr<nsIRequestContext> rc; + rv = rcsvc->NewRequestContext(getter_AddRefs(rc)); + if (NS_FAILED(rv)) { + MOZ_CRASH("NewRequestContext failed."); + } + rcID = rc->GetID(); + + rv = gHttpChannel->SetRequestContextID(rcID); + if (NS_FAILED(rv)) { + MOZ_CRASH("SetRequestContextID on gHttpChannel failed."); + } + + if (!proxyType.IsEmpty()) { + // NewProxiedChannel doesn't allow us to pass loadFlags directly + rv = gHttpChannel->SetLoadFlags(loadFlags); + if (rv != NS_OK) { + MOZ_CRASH("SetRequestMethod on gHttpChannel failed."); + } + } + + gStreamListener = new FuzzingStreamListener(); + gHttpChannel->AsyncOpen(gStreamListener); + + // Wait for StopRequest + gStreamListener->waitUntilDone(); + + bool mainPingBack = false; + + NS_DispatchBackgroundTask(NS_NewRunnableFunction("Dummy", [&]() { + NS_DispatchToMainThread( + NS_NewRunnableFunction("Dummy", [&]() { mainPingBack = true; })); + })); + + SpinEventLoopUntil("FuzzingRunNetworkHttp(mainPingBack)"_ns, + [&]() -> bool { return mainPingBack; }); + + channelRef = do_GetWeakReference(gHttpChannel); + } + + // Wait for the channel to be destroyed + SpinEventLoopUntil( + "FuzzingRunNetworkHttp(channel == nullptr)"_ns, [&]() -> bool { + nsCycleCollector_collect(CCReason::API, nullptr); + nsCOMPtr<nsIHttpChannel> channel = do_QueryReferent(channelRef); + return channel == nullptr; + }); + + if (!signalNetworkFuzzingDone()) { + // Wait for the connection to indicate closed + SpinEventLoopUntil("FuzzingRunNetworkHttp(gFuzzingConnClosed)"_ns, + [&]() -> bool { return gFuzzingConnClosed; }); + } + + rcsvc->RemoveRequestContext(rcID); + return 0; +} + +MOZ_FUZZING_INTERFACE_RAW(FuzzingInitNetworkHttp, FuzzingRunNetworkHttp, + NetworkHttp); + +MOZ_FUZZING_INTERFACE_RAW(FuzzingInitNetworkHttp2, FuzzingRunNetworkHttp, + NetworkHttp2); + +MOZ_FUZZING_INTERFACE_RAW(FuzzingInitNetworkHttp3, FuzzingRunNetworkHttp, + NetworkHttp3); + +MOZ_FUZZING_INTERFACE_RAW(FuzzingInitNetworkHttp2ProxyHttp2, + FuzzingRunNetworkHttp, NetworkHttp2ProxyHttp2); + +MOZ_FUZZING_INTERFACE_RAW(FuzzingInitNetworkHttpProxyHttp2, + FuzzingRunNetworkHttp, NetworkHttpProxyHttp2); + +MOZ_FUZZING_INTERFACE_RAW(FuzzingInitNetworkHttpProxyPlain, + FuzzingRunNetworkHttp, NetworkHttpProxyPlain); + +MOZ_FUZZING_INTERFACE_RAW(FuzzingInitNetworkHttp2ProxyPlain, + FuzzingRunNetworkHttp, NetworkHttp2ProxyPlain); + +} // namespace net +} // namespace mozilla diff --git a/netwerk/test/fuzz/TestURIFuzzing.cpp b/netwerk/test/fuzz/TestURIFuzzing.cpp new file mode 100644 index 0000000000..bbd76706fc --- /dev/null +++ b/netwerk/test/fuzz/TestURIFuzzing.cpp @@ -0,0 +1,240 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include <iostream> + +#include "FuzzingInterface.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsIURL.h" +#include "nsIStandardURL.h" +#include "nsIURIMutator.h" +#include "nsNetUtil.h" +#include "nsNetCID.h" +#include "nsPrintfCString.h" +#include "nsString.h" +#include "mozilla/Encoding.h" +#include "mozilla/Span.h" +#include "mozilla/Unused.h" + +template <typename T> +T get_numeric(char** buf, size_t* size) { + if (sizeof(T) > *size) { + return 0; + } + + T* iptr = reinterpret_cast<T*>(*buf); + *buf += sizeof(T); + *size -= sizeof(T); + return *iptr; +} + +nsAutoCString get_string(char** buf, size_t* size) { + uint8_t len = get_numeric<uint8_t>(buf, size); + if (len > *size) { + len = static_cast<uint8_t>(*size); + } + nsAutoCString str(*buf, len); + + *buf += len; + *size -= len; + return str; +} + +const char* charsets[] = { + "Big5", "EUC-JP", "EUC-KR", "gb18030", + "gbk", "IBM866", "ISO-2022-JP", "ISO-8859-10", + "ISO-8859-13", "ISO-8859-14", "ISO-8859-15", "ISO-8859-16", + "ISO-8859-2", "ISO-8859-3", "ISO-8859-4", "ISO-8859-5", + "ISO-8859-6", "ISO-8859-7", "ISO-8859-8", "ISO-8859-8-I", + "KOI8-R", "KOI8-U", "macintosh", "replacement", + "Shift_JIS", "UTF-16BE", "UTF-16LE", "UTF-8", + "windows-1250", "windows-1251", "windows-1252", "windows-1253", + "windows-1254", "windows-1255", "windows-1256", "windows-1257", + "windows-1258", "windows-874", "x-mac-cyrillic", "x-user-defined"}; + +static int FuzzingRunURIParser(const uint8_t* data, size_t size) { + char* buf = (char*)data; + + nsCOMPtr<nsIURI> uri; + nsAutoCString spec = get_string(&buf, &size); + + nsresult rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec(spec) + .Finalize(uri); + + if (NS_FAILED(rv)) { + return 0; + } + + uint8_t iters = get_numeric<uint8_t>(&buf, &size); + for (int i = 0; i < iters; i++) { + if (get_numeric<uint8_t>(&buf, &size) % 25 != 0) { + NS_MutateURI mutator(uri); + nsAutoCString acdata = get_string(&buf, &size); + + switch (get_numeric<uint8_t>(&buf, &size) % 12) { + default: + mutator.SetSpec(acdata); + break; + case 1: + mutator.SetScheme(acdata); + break; + case 2: + mutator.SetUserPass(acdata); + break; + case 3: + mutator.SetUsername(acdata); + break; + case 4: + mutator.SetPassword(acdata); + break; + case 5: + mutator.SetHostPort(acdata); + break; + case 6: + // Called via SetHostPort + mutator.SetHost(acdata); + break; + case 7: + // Called via multiple paths + mutator.SetPathQueryRef(acdata); + break; + case 8: + mutator.SetRef(acdata); + break; + case 9: + mutator.SetFilePath(acdata); + break; + case 10: + mutator.SetQuery(acdata); + break; + case 11: { + const uint8_t index = get_numeric<uint8_t>(&buf, &size) % + (sizeof(charsets) / sizeof(char*)); + const char* charset = charsets[index]; + auto encoding = mozilla::Encoding::ForLabelNoReplacement( + mozilla::MakeStringSpan(charset)); + mutator.SetQueryWithEncoding(acdata, encoding); + break; + } + } + + nsresult rv = mutator.Finalize(uri); + if (NS_FAILED(rv)) { + return 0; + } + } else { + nsAutoCString out; + + if (uri) { + switch (get_numeric<uint8_t>(&buf, &size) % 26) { + default: + uri->GetSpec(out); + break; + case 1: + uri->GetPrePath(out); + break; + case 2: + uri->GetScheme(out); + break; + case 3: + uri->GetUserPass(out); + break; + case 4: + uri->GetUsername(out); + break; + case 5: + uri->GetPassword(out); + break; + case 6: + uri->GetHostPort(out); + break; + case 7: + uri->GetHost(out); + break; + case 8: { + int rv; + uri->GetPort(&rv); + break; + } + case 9: + uri->GetPathQueryRef(out); + break; + case 10: { + nsCOMPtr<nsIURI> other; + bool rv; + nsAutoCString spec = get_string(&buf, &size); + NS_NewURI(getter_AddRefs(other), spec); + uri->Equals(other, &rv); + break; + } + case 11: { + nsAutoCString scheme = get_string(&buf, &size); + bool rv; + uri->SchemeIs("https", &rv); + break; + } + case 12: { + nsAutoCString in = get_string(&buf, &size); + uri->Resolve(in, out); + break; + } + case 13: + uri->GetAsciiSpec(out); + break; + case 14: + uri->GetAsciiHostPort(out); + break; + case 15: + uri->GetAsciiHost(out); + break; + case 16: + uri->GetRef(out); + break; + case 17: { + nsCOMPtr<nsIURI> other; + bool rv; + nsAutoCString spec = get_string(&buf, &size); + NS_NewURI(getter_AddRefs(other), spec); + uri->EqualsExceptRef(other, &rv); + break; + } + case 18: + uri->GetSpecIgnoringRef(out); + break; + case 19: { + bool rv; + uri->GetHasRef(&rv); + break; + } + case 20: + uri->GetFilePath(out); + break; + case 21: + uri->GetQuery(out); + break; + case 22: + uri->GetDisplayHost(out); + break; + case 23: + uri->GetDisplayHostPort(out); + break; + case 24: + uri->GetDisplaySpec(out); + break; + case 25: + uri->GetDisplayPrePath(out); + break; + } + } + } + } + + return 0; +} + +MOZ_FUZZING_INTERFACE_RAW(nullptr, FuzzingRunURIParser, URIParser); diff --git a/netwerk/test/fuzz/TestWebsocketFuzzing.cpp b/netwerk/test/fuzz/TestWebsocketFuzzing.cpp new file mode 100644 index 0000000000..89fd08859f --- /dev/null +++ b/netwerk/test/fuzz/TestWebsocketFuzzing.cpp @@ -0,0 +1,229 @@ +#include "mozilla/Preferences.h" + +#include "FuzzingInterface.h" +#include "FuzzyLayer.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsCycleCollector.h" +#include "nsIPrincipal.h" +#include "nsIWebSocketChannel.h" +#include "nsIWebSocketListener.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsString.h" +#include "nsScriptSecurityManager.h" +#include "nsServiceManagerUtils.h" +#include "NullPrincipal.h" +#include "nsSandboxFlags.h" + +namespace mozilla { +namespace net { + +// Used to determine if the fuzzing target should use https:// in spec. +static bool fuzzWSS = true; + +class FuzzingWebSocketListener final : public nsIWebSocketListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWEBSOCKETLISTENER + + FuzzingWebSocketListener() = default; + + void waitUntilDoneOrStarted() { + SpinEventLoopUntil("FuzzingWebSocketListener::waitUntilDoneOrStarted"_ns, + [&]() { return mChannelDone || mChannelStarted; }); + } + + void waitUntilDone() { + SpinEventLoopUntil("FuzzingWebSocketListener::waitUntilDone"_ns, + [&]() { return mChannelDone; }); + } + + void waitUntilDoneOrAck() { + SpinEventLoopUntil("FuzzingWebSocketListener::waitUntilDoneOrAck"_ns, + [&]() { return mChannelDone || mChannelAck; }); + } + + bool isStarted() { return mChannelStarted; } + + private: + ~FuzzingWebSocketListener() = default; + bool mChannelDone = false; + bool mChannelStarted = false; + bool mChannelAck = false; +}; + +NS_IMPL_ISUPPORTS(FuzzingWebSocketListener, nsIWebSocketListener) + +NS_IMETHODIMP +FuzzingWebSocketListener::OnStart(nsISupports* aContext) { + FUZZING_LOG(("FuzzingWebSocketListener::OnStart")); + mChannelStarted = true; + return NS_OK; +} + +NS_IMETHODIMP +FuzzingWebSocketListener::OnStop(nsISupports* aContext, nsresult aStatusCode) { + FUZZING_LOG(("FuzzingWebSocketListener::OnStop")); + mChannelDone = true; + return NS_OK; +} + +NS_IMETHODIMP +FuzzingWebSocketListener::OnAcknowledge(nsISupports* aContext, uint32_t aSize) { + FUZZING_LOG(("FuzzingWebSocketListener::OnAcknowledge")); + mChannelAck = true; + return NS_OK; +} + +NS_IMETHODIMP +FuzzingWebSocketListener::OnServerClose(nsISupports* aContext, uint16_t aCode, + const nsACString& aReason) { + FUZZING_LOG(("FuzzingWebSocketListener::OnServerClose")); + return NS_OK; +} + +NS_IMETHODIMP +FuzzingWebSocketListener::OnMessageAvailable(nsISupports* aContext, + const nsACString& aMsg) { + FUZZING_LOG(("FuzzingWebSocketListener::OnMessageAvailable")); + return NS_OK; +} + +NS_IMETHODIMP +FuzzingWebSocketListener::OnBinaryMessageAvailable(nsISupports* aContext, + const nsACString& aMsg) { + FUZZING_LOG(("FuzzingWebSocketListener::OnBinaryMessageAvailable")); + return NS_OK; +} + +NS_IMETHODIMP +FuzzingWebSocketListener::OnError() { + FUZZING_LOG(("FuzzingWebSocketListener::OnError")); + return NS_OK; +} + +static int FuzzingInitNetworkWebsocket(int* argc, char*** argv) { + Preferences::SetBool("network.dns.native-is-localhost", true); + Preferences::SetBool("fuzzing.necko.enabled", true); + Preferences::SetBool("network.websocket.delay-failed-reconnects", false); + Preferences::SetInt("network.http.speculative-parallel-limit", 0); + Preferences::SetInt("network.proxy.type", 0); // PROXYCONFIG_DIRECT + return 0; +} + +static int FuzzingInitNetworkWebsocketPlain(int* argc, char*** argv) { + fuzzWSS = false; + return FuzzingInitNetworkWebsocket(argc, argv); +} + +static int FuzzingRunNetworkWebsocket(const uint8_t* data, size_t size) { + // Set the data to be processed + addNetworkFuzzingBuffer(data, size); + + nsWeakPtr channelRef; + + { + nsresult rv; + + nsSecurityFlags secFlags; + secFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; + uint32_t sandboxFlags = SANDBOXED_ORIGIN; + + nsCOMPtr<nsIURI> url; + nsAutoCString spec; + RefPtr<FuzzingWebSocketListener> gWebSocketListener; + nsCOMPtr<nsIWebSocketChannel> gWebSocketChannel; + + if (fuzzWSS) { + spec = "https://127.0.0.1/"; + gWebSocketChannel = + do_CreateInstance("@mozilla.org/network/protocol;1?name=wss", &rv); + } else { + spec = "http://127.0.0.1/"; + gWebSocketChannel = + do_CreateInstance("@mozilla.org/network/protocol;1?name=ws", &rv); + } + + if (rv != NS_OK) { + MOZ_CRASH("Failed to create WebSocketChannel"); + } + + if (NS_NewURI(getter_AddRefs(url), spec) != NS_OK) { + MOZ_CRASH("Call to NS_NewURI failed."); + } + + nsCOMPtr<nsIPrincipal> nullPrincipal = + NullPrincipal::CreateWithoutOriginAttributes(); + + rv = gWebSocketChannel->InitLoadInfoNative( + nullptr, nullPrincipal, nsContentUtils::GetSystemPrincipal(), nullptr, + secFlags, nsIContentPolicy::TYPE_WEBSOCKET, sandboxFlags); + + if (rv != NS_OK) { + MOZ_CRASH("Failed to call InitLoadInfo"); + } + + gWebSocketListener = new FuzzingWebSocketListener(); + + OriginAttributes attrs; + rv = gWebSocketChannel->AsyncOpenNative(url, spec, attrs, 0, + gWebSocketListener, nullptr); + + if (rv == NS_OK) { + FUZZING_LOG(("Successful call to AsyncOpen")); + + // Wait for StartRequest or StopRequest + gWebSocketListener->waitUntilDoneOrStarted(); + + if (gWebSocketListener->isStarted()) { + rv = gWebSocketChannel->SendBinaryMsg("Hello world"_ns); + + if (rv != NS_OK) { + FUZZING_LOG(("Warning: Failed to call SendBinaryMsg")); + } else { + gWebSocketListener->waitUntilDoneOrAck(); + } + + rv = gWebSocketChannel->Close(1000, ""_ns); + + if (rv != NS_OK) { + FUZZING_LOG(("Warning: Failed to call close")); + } + } + + // Wait for StopRequest + gWebSocketListener->waitUntilDone(); + } else { + FUZZING_LOG(("Warning: Failed to call AsyncOpen")); + } + + channelRef = do_GetWeakReference(gWebSocketChannel); + } + + // Wait for the channel to be destroyed + SpinEventLoopUntil( + "FuzzingRunNetworkWebsocket(channel == nullptr)"_ns, [&]() -> bool { + nsCycleCollector_collect(CCReason::API, nullptr); + nsCOMPtr<nsIWebSocketChannel> channel = do_QueryReferent(channelRef); + return channel == nullptr; + }); + + if (!signalNetworkFuzzingDone()) { + // Wait for the connection to indicate closed + SpinEventLoopUntil("FuzzingRunNetworkWebsocket(gFuzzingConnClosed)"_ns, + [&]() -> bool { return gFuzzingConnClosed; }); + } + + return 0; +} + +MOZ_FUZZING_INTERFACE_RAW(FuzzingInitNetworkWebsocket, + FuzzingRunNetworkWebsocket, NetworkWebsocket); +MOZ_FUZZING_INTERFACE_RAW(FuzzingInitNetworkWebsocketPlain, + FuzzingRunNetworkWebsocket, NetworkWebsocketPlain); + +} // namespace net +} // namespace mozilla diff --git a/netwerk/test/fuzz/moz.build b/netwerk/test/fuzz/moz.build new file mode 100644 index 0000000000..9ff7eab923 --- /dev/null +++ b/netwerk/test/fuzz/moz.build @@ -0,0 +1,31 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES += [ + "FuzzingStreamListener.cpp", + "TestHttpFuzzing.cpp", + "TestURIFuzzing.cpp", + "TestWebsocketFuzzing.cpp", +] + +LOCAL_INCLUDES += [ + "/caps", + "/netwerk/base", + "/netwerk/protocol/http", + "/xpcom/tests/gtest", +] + +EXPORTS.mozilla.fuzzing += [ + "FuzzingStreamListener.h", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += ["!/xpcom", "/xpcom/components"] + +include("/tools/fuzzing/libfuzzer-config.mozbuild") diff --git a/netwerk/test/fuzz/url_tokens.dict b/netwerk/test/fuzz/url_tokens.dict new file mode 100644 index 0000000000..f297ad5de7 --- /dev/null +++ b/netwerk/test/fuzz/url_tokens.dict @@ -0,0 +1,51 @@ +### netwerk/base/nsStandardURL.cpp +# Control characters +" " +"#" +"/" +":" +"?" +"@" +"[" +"\\" +"]" +"*" +"<" +">" +"|" +"\\" + +# URI schemes +"about" +"android" +"blob" +"chrome" +"data" +"file" +"ftp" +"http" +"https" +"indexeddb" +"jar" +"javascript" +"moz" +"moz-safe-about" +"page" +"resource" +"sftp" +"smb" +"ssh" +"view" +"ws" +"wss" + +# URI Hosts +"selfuri.com" +"127.0.0.1" +"::1" + +# about protocol safe paths +"blank" +"license" +"logo" +"srcdoc" diff --git a/netwerk/test/gtest/TestBase64Stream.cpp b/netwerk/test/gtest/TestBase64Stream.cpp new file mode 100644 index 0000000000..47ef9e7bc6 --- /dev/null +++ b/netwerk/test/gtest/TestBase64Stream.cpp @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mozilla/Base64.h" +#include "mozilla/gtest/MozAssertions.h" +#include "nsCOMPtr.h" +#include "nsIInputStream.h" +#include "nsStringStream.h" + +namespace mozilla { +namespace net { + +// An input stream whose ReadSegments method calls aWriter with writes of size +// aStep from the provided aInput in order to test edge-cases related to small +// buffers. +class TestStream final : public nsIInputStream { + public: + NS_DECL_ISUPPORTS; + + TestStream(const nsACString& aInput, uint32_t aStep) + : mInput(aInput), mStep(aStep) {} + + NS_IMETHOD Close() override { MOZ_CRASH("This should not be called"); } + + NS_IMETHOD Available(uint64_t* aLength) override { + *aLength = mInput.Length() - mPos; + return NS_OK; + } + + NS_IMETHOD StreamStatus() override { return NS_OK; } + + NS_IMETHOD Read(char* aBuffer, uint32_t aCount, + uint32_t* aReadCount) override { + MOZ_CRASH("This should not be called"); + } + + NS_IMETHOD ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* aResult) override { + *aResult = 0; + + if (mPos == mInput.Length()) { + return NS_OK; + } + + while (aCount > 0) { + uint32_t amt = std::min(mStep, (uint32_t)(mInput.Length() - mPos)); + + uint32_t read = 0; + nsresult rv = + aWriter(this, aClosure, mInput.get() + mPos, *aResult, amt, &read); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aResult += read; + aCount -= read; + mPos += read; + } + + return NS_OK; + } + + NS_IMETHOD IsNonBlocking(bool* aNonBlocking) override { + *aNonBlocking = true; + return NS_OK; + } + + private: + ~TestStream() = default; + + nsCString mInput; + const uint32_t mStep; + uint32_t mPos = 0; +}; + +NS_IMPL_ISUPPORTS(TestStream, nsIInputStream) + +// Test the base64 encoder with writer buffer sizes between 1 byte and the +// entire length of "Hello World!" in order to exercise various edge cases. +TEST(TestBase64Stream, Run) +{ + nsCString input; + input.AssignLiteral("Hello World!"); + + for (uint32_t step = 1; step <= input.Length(); ++step) { + RefPtr<TestStream> ts = new TestStream(input, step); + + nsAutoString encodedData; + nsresult rv = Base64EncodeInputStream(ts, encodedData, input.Length()); + ASSERT_NS_SUCCEEDED(rv); + + EXPECT_TRUE(encodedData.EqualsLiteral("SGVsbG8gV29ybGQh")); + } +} + +TEST(TestBase64Stream, VaryingCount) +{ + nsCString input; + input.AssignLiteral("Hello World!"); + + std::pair<size_t, nsCString> tests[] = { + {0, "SGVsbG8gV29ybGQh"_ns}, {1, "SA=="_ns}, + {5, "SGVsbG8="_ns}, {11, "SGVsbG8gV29ybGQ="_ns}, + {12, "SGVsbG8gV29ybGQh"_ns}, {13, "SGVsbG8gV29ybGQh"_ns}, + }; + + for (auto& [count, expected] : tests) { + nsCOMPtr<nsIInputStream> is; + nsresult rv = NS_NewCStringInputStream(getter_AddRefs(is), input); + ASSERT_NS_SUCCEEDED(rv); + + nsAutoCString encodedData; + rv = Base64EncodeInputStream(is, encodedData, count); + ASSERT_NS_SUCCEEDED(rv); + EXPECT_EQ(encodedData, expected) << "count: " << count; + } +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/test/gtest/TestBind.cpp b/netwerk/test/gtest/TestBind.cpp new file mode 100644 index 0000000000..371a09fdab --- /dev/null +++ b/netwerk/test/gtest/TestBind.cpp @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TestCommon.h" +#include "gtest/gtest.h" +#include "mozilla/gtest/MozAssertions.h" +#include "nsISocketTransportService.h" +#include "nsISocketTransport.h" +#include "nsIServerSocket.h" +#include "nsIAsyncInputStream.h" +#include "mozilla/net/DNS.h" +#include "prerror.h" +#include "../../base/nsSocketTransportService2.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" + +using namespace mozilla::net; +using namespace mozilla; + +class ServerListener : public nsIServerSocketListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISERVERSOCKETLISTENER + + explicit ServerListener(WaitForCondition* waiter); + + // Port that is got from server side will be store here. + uint32_t mClientPort; + bool mFailed; + RefPtr<WaitForCondition> mWaiter; + + private: + virtual ~ServerListener(); +}; + +NS_IMPL_ISUPPORTS(ServerListener, nsIServerSocketListener) + +ServerListener::ServerListener(WaitForCondition* waiter) + : mClientPort(-1), mFailed(false), mWaiter(waiter) {} + +ServerListener::~ServerListener() = default; + +NS_IMETHODIMP +ServerListener::OnSocketAccepted(nsIServerSocket* aServ, + nsISocketTransport* aTransport) { + // Run on STS thread. + NetAddr peerAddr; + nsresult rv = aTransport->GetPeerAddr(&peerAddr); + if (NS_FAILED(rv)) { + mFailed = true; + mWaiter->Notify(); + return NS_OK; + } + mClientPort = PR_ntohs(peerAddr.inet.port); + mWaiter->Notify(); + return NS_OK; +} + +NS_IMETHODIMP +ServerListener::OnStopListening(nsIServerSocket* aServ, nsresult aStatus) { + return NS_OK; +} + +class ClientInputCallback : public nsIInputStreamCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAMCALLBACK + + explicit ClientInputCallback(WaitForCondition* waiter); + + bool mFailed; + RefPtr<WaitForCondition> mWaiter; + + private: + virtual ~ClientInputCallback(); +}; + +NS_IMPL_ISUPPORTS(ClientInputCallback, nsIInputStreamCallback) + +ClientInputCallback::ClientInputCallback(WaitForCondition* waiter) + : mFailed(false), mWaiter(waiter) {} + +ClientInputCallback::~ClientInputCallback() = default; + +NS_IMETHODIMP +ClientInputCallback::OnInputStreamReady(nsIAsyncInputStream* aStream) { + // Server doesn't send. That means if we are here, we probably have run into + // an error. + uint64_t avail; + nsresult rv = aStream->Available(&avail); + if (NS_FAILED(rv)) { + mFailed = true; + } + mWaiter->Notify(); + return NS_OK; +} + +TEST(TestBind, MainTest) +{ + // + // Server side. + // + nsCOMPtr<nsIServerSocket> server = + do_CreateInstance("@mozilla.org/network/server-socket;1"); + ASSERT_TRUE(server); + + nsresult rv = server->Init(-1, true, -1); + ASSERT_NS_SUCCEEDED(rv); + + int32_t serverPort; + rv = server->GetPort(&serverPort); + ASSERT_NS_SUCCEEDED(rv); + + RefPtr<WaitForCondition> waiter = new WaitForCondition(); + + // Listening. + RefPtr<ServerListener> serverListener = new ServerListener(waiter); + rv = server->AsyncListen(serverListener); + ASSERT_NS_SUCCEEDED(rv); + + // + // Client side + // + uint32_t bindingPort = 20000; + nsCOMPtr<nsISocketTransportService> service = + do_GetService("@mozilla.org/network/socket-transport-service;1", &rv); + ASSERT_NS_SUCCEEDED(rv); + + nsCOMPtr<nsIInputStream> inputStream; + RefPtr<ClientInputCallback> clientCallback; + + auto* sts = gSocketTransportService; + ASSERT_TRUE(sts); + for (int32_t tried = 0; tried < 100; tried++) { + NS_DispatchAndSpinEventLoopUntilComplete( + "test"_ns, sts, NS_NewRunnableFunction("test", [&]() { + nsCOMPtr<nsISocketTransport> client; + rv = service->CreateTransport(nsTArray<nsCString>(), "127.0.0.1"_ns, + serverPort, nullptr, nullptr, + getter_AddRefs(client)); + ASSERT_NS_SUCCEEDED(rv); + + // Bind to a port. It's possible that we are binding to a port + // that is currently in use. If we failed to bind, we try next + // port. + NetAddr bindingAddr; + bindingAddr.inet.family = AF_INET; + bindingAddr.inet.ip = 0; + bindingAddr.inet.port = PR_htons(bindingPort); + rv = client->Bind(&bindingAddr); + ASSERT_NS_SUCCEEDED(rv); + + // Open IO streams, to make client SocketTransport connect to + // server. + clientCallback = new ClientInputCallback(waiter); + rv = client->OpenInputStream(nsITransport::OPEN_UNBUFFERED, 0, 0, + getter_AddRefs(inputStream)); + ASSERT_NS_SUCCEEDED(rv); + + nsCOMPtr<nsIAsyncInputStream> asyncInputStream = + do_QueryInterface(inputStream); + rv = asyncInputStream->AsyncWait(clientCallback, 0, 0, nullptr); + })); + + // Wait for server's response or callback of input stream. + waiter->Wait(1); + if (clientCallback->mFailed) { + // if client received error, we likely have bound a port that is + // in use. we can try another port. + bindingPort++; + } else { + // We are unlocked by server side, leave the loop and check + // result. + break; + } + } + + ASSERT_FALSE(serverListener->mFailed); + ASSERT_EQ(serverListener->mClientPort, bindingPort); + + inputStream->Close(); + waiter->Wait(1); + ASSERT_TRUE(clientCallback->mFailed); + + server->Close(); +} diff --git a/netwerk/test/gtest/TestBufferedInputStream.cpp b/netwerk/test/gtest/TestBufferedInputStream.cpp new file mode 100644 index 0000000000..7230fe7e5b --- /dev/null +++ b/netwerk/test/gtest/TestBufferedInputStream.cpp @@ -0,0 +1,252 @@ +#include "gtest/gtest.h" + +#include "mozilla/SpinEventLoopUntil.h" +#include "nsBufferedStreams.h" +#include "nsIThread.h" +#include "nsNetUtil.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "Helpers.h" + +// Helper function for creating a testing::AsyncStringStream +already_AddRefed<nsBufferedInputStream> CreateStream(uint32_t aSize, + nsCString& aBuffer) { + aBuffer.SetLength(aSize); + for (uint32_t i = 0; i < aSize; ++i) { + aBuffer.BeginWriting()[i] = i % 10; + } + + nsCOMPtr<nsIInputStream> stream = new testing::AsyncStringStream(aBuffer); + + RefPtr<nsBufferedInputStream> bis = new nsBufferedInputStream(); + bis->Init(stream, aSize); + return bis.forget(); +} + +// Simple reading. +TEST(TestBufferedInputStream, SimpleRead) +{ + const size_t kBufSize = 10; + + nsCString buf; + RefPtr<nsBufferedInputStream> bis = CreateStream(kBufSize, buf); + + uint64_t length; + ASSERT_EQ(NS_OK, bis->Available(&length)); + ASSERT_EQ((uint64_t)kBufSize, length); + + char buf2[kBufSize]; + uint32_t count; + ASSERT_EQ(NS_OK, bis->Read(buf2, sizeof(buf2), &count)); + ASSERT_EQ(count, buf.Length()); + ASSERT_TRUE(nsCString(buf.get(), kBufSize).Equals(nsCString(buf2, count))); +} + +// Simple segment reading. +TEST(TestBufferedInputStream, SimpleReadSegments) +{ + const size_t kBufSize = 10; + + nsCString buf; + RefPtr<nsBufferedInputStream> bis = CreateStream(kBufSize, buf); + + char buf2[kBufSize]; + uint32_t count; + ASSERT_EQ(NS_OK, bis->ReadSegments(NS_CopySegmentToBuffer, buf2, sizeof(buf2), + &count)); + ASSERT_EQ(count, buf.Length()); + ASSERT_TRUE(nsCString(buf.get(), kBufSize).Equals(nsCString(buf2, count))); +} + +// AsyncWait - sync +TEST(TestBufferedInputStream, AsyncWait_sync) +{ + const size_t kBufSize = 10; + + nsCString buf; + RefPtr<nsBufferedInputStream> bis = CreateStream(kBufSize, buf); + + RefPtr<testing::InputStreamCallback> cb = new testing::InputStreamCallback(); + + ASSERT_EQ(NS_OK, bis->AsyncWait(cb, 0, 0, nullptr)); + + // Immediatelly called + ASSERT_TRUE(cb->Called()); +} + +// AsyncWait - async +TEST(TestBufferedInputStream, AsyncWait_async) +{ + const size_t kBufSize = 10; + + nsCString buf; + RefPtr<nsBufferedInputStream> bis = CreateStream(kBufSize, buf); + + RefPtr<testing::InputStreamCallback> cb = new testing::InputStreamCallback(); + nsCOMPtr<nsIThread> thread = do_GetCurrentThread(); + + ASSERT_EQ(NS_OK, bis->AsyncWait(cb, 0, 0, thread)); + + ASSERT_FALSE(cb->Called()); + + // Eventually it is called. + MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil( + "TEST(TestBufferedInputStream, AsyncWait_async)"_ns, + [&]() { return cb->Called(); })); + ASSERT_TRUE(cb->Called()); +} + +// AsyncWait - sync - closureOnly +TEST(TestBufferedInputStream, AsyncWait_sync_closureOnly) +{ + const size_t kBufSize = 10; + + nsCString buf; + RefPtr<nsBufferedInputStream> bis = CreateStream(kBufSize, buf); + + RefPtr<testing::InputStreamCallback> cb = new testing::InputStreamCallback(); + + ASSERT_EQ(NS_OK, bis->AsyncWait(cb, nsIAsyncInputStream::WAIT_CLOSURE_ONLY, 0, + nullptr)); + ASSERT_FALSE(cb->Called()); + + bis->CloseWithStatus(NS_ERROR_FAILURE); + + // Immediatelly called + ASSERT_TRUE(cb->Called()); +} + +// AsyncWait - async +TEST(TestBufferedInputStream, AsyncWait_async_closureOnly) +{ + const size_t kBufSize = 10; + + nsCString buf; + RefPtr<nsBufferedInputStream> bis = CreateStream(kBufSize, buf); + + RefPtr<testing::InputStreamCallback> cb = new testing::InputStreamCallback(); + nsCOMPtr<nsIThread> thread = do_GetCurrentThread(); + + ASSERT_EQ(NS_OK, bis->AsyncWait(cb, nsIAsyncInputStream::WAIT_CLOSURE_ONLY, 0, + thread)); + + ASSERT_FALSE(cb->Called()); + bis->CloseWithStatus(NS_ERROR_FAILURE); + ASSERT_FALSE(cb->Called()); + + // Eventually it is called. + MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil( + "TEST(TestBufferedInputStream, AsyncWait_async_closureOnly)"_ns, + [&]() { return cb->Called(); })); + ASSERT_TRUE(cb->Called()); +} + +TEST(TestBufferedInputStream, AsyncWait_after_close) +{ + const size_t kBufSize = 10; + + nsCString buf; + RefPtr<nsBufferedInputStream> bis = CreateStream(kBufSize, buf); + + nsCOMPtr<nsIThread> eventTarget = do_GetCurrentThread(); + + auto cb = mozilla::MakeRefPtr<testing::InputStreamCallback>(); + ASSERT_EQ(NS_OK, bis->AsyncWait(cb, 0, 0, eventTarget)); + MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil( + "TEST(TestBufferedInputStream, AsyncWait_after_close) 1"_ns, + [&]() { return cb->Called(); })); + ASSERT_TRUE(cb->Called()); + + ASSERT_EQ(NS_OK, bis->Close()); + + cb = mozilla::MakeRefPtr<testing::InputStreamCallback>(); + ASSERT_EQ(NS_OK, bis->AsyncWait(cb, 0, 0, eventTarget)); + MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil( + "TEST(TestBufferedInputStream, AsyncWait_after_close) 2"_ns, + [&]() { return cb->Called(); })); + ASSERT_TRUE(cb->Called()); +} + +TEST(TestBufferedInputStream, AsyncLengthWait_after_close) +{ + nsCString buf{"The Quick Brown Fox Jumps over the Lazy Dog"}; + const size_t kBufSize = 44; + + RefPtr<nsBufferedInputStream> bis = CreateStream(kBufSize, buf); + + nsCOMPtr<nsIThread> eventTarget = do_GetCurrentThread(); + + auto cb = mozilla::MakeRefPtr<testing::LengthCallback>(); + ASSERT_EQ(NS_OK, bis->AsyncLengthWait(cb, eventTarget)); + MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil( + "TEST(TestBufferedInputStream, AsyncLengthWait_after_close) 1"_ns, + [&]() { return cb->Called(); })); + ASSERT_TRUE(cb->Called()); + + uint64_t length; + ASSERT_EQ(NS_OK, bis->Available(&length)); + ASSERT_EQ((uint64_t)kBufSize, length); + + cb = mozilla::MakeRefPtr<testing::LengthCallback>(); + ASSERT_EQ(NS_OK, bis->AsyncLengthWait(cb, eventTarget)); + MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil( + "TEST(TestBufferedInputStream, AsyncLengthWait_after_close) 2"_ns, + [&]() { return cb->Called(); })); + ASSERT_TRUE(cb->Called()); +} + +// This stream returns a few bytes on the first read, and error on the second. +class BrokenInputStream : public nsIInputStream { + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + private: + virtual ~BrokenInputStream() = default; + bool mFirst = true; +}; + +NS_IMPL_ISUPPORTS(BrokenInputStream, nsIInputStream) + +NS_IMETHODIMP BrokenInputStream::Close(void) { return NS_OK; } + +NS_IMETHODIMP BrokenInputStream::Available(uint64_t* _retval) { + *_retval = 100; + return NS_OK; +} + +NS_IMETHODIMP BrokenInputStream::StreamStatus(void) { return NS_OK; } + +NS_IMETHODIMP BrokenInputStream::Read(char* aBuf, uint32_t aCount, + uint32_t* _retval) { + if (mFirst) { + aBuf[0] = 'h'; + aBuf[1] = 'e'; + aBuf[2] = 'l'; + aBuf[3] = 0; + *_retval = 4; + mFirst = false; + return NS_OK; + } + return NS_ERROR_CORRUPTED_CONTENT; +} + +NS_IMETHODIMP BrokenInputStream::ReadSegments(nsWriteSegmentFun aWriter, + void* aClosure, uint32_t aCount, + uint32_t* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP BrokenInputStream::IsNonBlocking(bool* _retval) { + *_retval = false; + return NS_OK; +} + +// Check that the error from BrokenInputStream::Read is propagated +// through NS_ReadInputStreamToString +TEST(TestBufferedInputStream, BrokenInputStreamToBuffer) +{ + nsAutoCString out; + RefPtr<BrokenInputStream> stream = new BrokenInputStream(); + + nsresult rv = NS_ReadInputStreamToString(stream, out, -1); + ASSERT_EQ(rv, NS_ERROR_CORRUPTED_CONTENT); +} diff --git a/netwerk/test/gtest/TestCommon.cpp b/netwerk/test/gtest/TestCommon.cpp new file mode 100644 index 0000000000..37c08fbed8 --- /dev/null +++ b/netwerk/test/gtest/TestCommon.cpp @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TestCommon.h" + +NS_IMPL_ISUPPORTS(WaitForCondition, nsIRunnable) diff --git a/netwerk/test/gtest/TestCommon.h b/netwerk/test/gtest/TestCommon.h new file mode 100644 index 0000000000..0d2fd74e5b --- /dev/null +++ b/netwerk/test/gtest/TestCommon.h @@ -0,0 +1,45 @@ +/* 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/. */ + +#ifndef TestCommon_h__ +#define TestCommon_h__ + +#include <stdlib.h> +#include "nsThreadUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/SpinEventLoopUntil.h" + +//----------------------------------------------------------------------------- + +class WaitForCondition final : public nsIRunnable { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + void Wait(int pending) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPending == 0); + + mPending = pending; + mozilla::SpinEventLoopUntil("TestCommon.h:WaitForCondition::Wait"_ns, + [&]() { return !mPending; }); + NS_ProcessPendingEvents(nullptr); + } + + void Notify() { NS_DispatchToMainThread(this); } + + private: + virtual ~WaitForCondition() = default; + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPending); + + --mPending; + return NS_OK; + } + + uint32_t mPending = 0; +}; + +#endif diff --git a/netwerk/test/gtest/TestCookie.cpp b/netwerk/test/gtest/TestCookie.cpp new file mode 100644 index 0000000000..4812ee47f1 --- /dev/null +++ b/netwerk/test/gtest/TestCookie.cpp @@ -0,0 +1,1126 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TestCommon.h" +#include "gtest/gtest.h" +#include "nsContentUtils.h" +#include "nsICookieService.h" +#include "nsICookieManager.h" +#include "nsICookie.h" +#include <stdio.h> +#include "plstr.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" +#include "nsIPrincipal.h" +#include "nsIScriptSecurityManager.h" +#include "nsServiceManagerUtils.h" +#include "nsNetCID.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "mozilla/dom/Document.h" +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/Preferences.h" +#include "mozilla/Unused.h" +#include "mozilla/net/CookieJarSettings.h" +#include "Cookie.h" +#include "nsIURI.h" + +using namespace mozilla; +using namespace mozilla::net; + +static NS_DEFINE_CID(kCookieServiceCID, NS_COOKIESERVICE_CID); +static NS_DEFINE_CID(kPrefServiceCID, NS_PREFSERVICE_CID); + +// various pref strings +static const char kCookiesPermissions[] = "network.cookie.cookieBehavior"; +static const char kPrefCookieQuotaPerHost[] = "network.cookie.quotaPerHost"; +static const char kCookiesMaxPerHost[] = "network.cookie.maxPerHost"; + +#define OFFSET_ONE_WEEK int64_t(604800) * PR_USEC_PER_SEC +#define OFFSET_ONE_DAY int64_t(86400) * PR_USEC_PER_SEC + +// Set server time or expiry time +void SetTime(PRTime offsetTime, nsAutoCString& serverString, + nsAutoCString& cookieString, bool expiry) { + char timeStringPreset[40]; + PRTime CurrentTime = PR_Now(); + PRTime SetCookieTime = CurrentTime + offsetTime; + PRTime SetExpiryTime; + if (expiry) { + SetExpiryTime = SetCookieTime - OFFSET_ONE_DAY; + } else { + SetExpiryTime = SetCookieTime + OFFSET_ONE_DAY; + } + + // Set server time string + PRExplodedTime explodedTime; + PR_ExplodeTime(SetCookieTime, PR_GMTParameters, &explodedTime); + PR_FormatTimeUSEnglish(timeStringPreset, 40, "%c GMT", &explodedTime); + serverString.Assign(timeStringPreset); + + // Set cookie string + PR_ExplodeTime(SetExpiryTime, PR_GMTParameters, &explodedTime); + PR_FormatTimeUSEnglish(timeStringPreset, 40, "%c GMT", &explodedTime); + cookieString.ReplaceLiteral( + 0, strlen("test=expiry; expires=") + strlen(timeStringPreset) + 1, + "test=expiry; expires="); + cookieString.Append(timeStringPreset); +} + +void SetACookieInternal(nsICookieService* aCookieService, const char* aSpec, + const char* aCookieString, bool aAllowed) { + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), aSpec); + + // We create a dummy channel using the aSpec to simulate same-siteness + nsresult rv0; + nsCOMPtr<nsIScriptSecurityManager> ssm = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv0); + ASSERT_NS_SUCCEEDED(rv0); + nsCOMPtr<nsIPrincipal> specPrincipal; + nsCString tmpString(aSpec); + ssm->CreateContentPrincipalFromOrigin(tmpString, + getter_AddRefs(specPrincipal)); + + nsCOMPtr<nsIChannel> dummyChannel; + NS_NewChannel(getter_AddRefs(dummyChannel), uri, specPrincipal, + nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, + nsIContentPolicy::TYPE_OTHER); + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = + aAllowed + ? CookieJarSettings::Create(CookieJarSettings::eRegular, + /* shouldResistFingerprinting */ false) + : CookieJarSettings::GetBlockingAll( + /* shouldResistFingerprinting */ false); + MOZ_ASSERT(cookieJarSettings); + + nsCOMPtr<nsILoadInfo> loadInfo = dummyChannel->LoadInfo(); + loadInfo->SetCookieJarSettings(cookieJarSettings); + + nsresult rv = aCookieService->SetCookieStringFromHttp( + uri, nsDependentCString(aCookieString), dummyChannel); + EXPECT_NS_SUCCEEDED(rv); +} + +void SetACookieJarBlocked(nsICookieService* aCookieService, const char* aSpec, + const char* aCookieString) { + SetACookieInternal(aCookieService, aSpec, aCookieString, false); +} + +void SetACookie(nsICookieService* aCookieService, const char* aSpec, + const char* aCookieString) { + SetACookieInternal(aCookieService, aSpec, aCookieString, true); +} + +// The cookie string is returned via aCookie. +void GetACookie(nsICookieService* aCookieService, const char* aSpec, + nsACString& aCookie) { + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), aSpec); + + nsCOMPtr<nsIIOService> service = do_GetIOService(); + + nsCOMPtr<nsIChannel> channel; + Unused << service->NewChannelFromURI( + uri, nullptr, nsContentUtils::GetSystemPrincipal(), + nsContentUtils::GetSystemPrincipal(), 0, nsIContentPolicy::TYPE_DOCUMENT, + getter_AddRefs(channel)); + + Unused << aCookieService->GetCookieStringFromHttp(uri, channel, aCookie); +} + +// The cookie string is returned via aCookie. +void GetACookieNoHttp(nsICookieService* aCookieService, const char* aSpec, + nsACString& aCookie) { + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), aSpec); + + RefPtr<BasePrincipal> principal = + BasePrincipal::CreateContentPrincipal(uri, OriginAttributes()); + MOZ_ASSERT(principal); + + nsCOMPtr<mozilla::dom::Document> document; + nsresult rv = NS_NewDOMDocument(getter_AddRefs(document), + u""_ns, // aNamespaceURI + u""_ns, // aQualifiedName + nullptr, // aDoctype + uri, uri, principal, + false, // aLoadedAsData + nullptr, // aEventObject + DocumentFlavorHTML); + Unused << NS_WARN_IF(NS_FAILED(rv)); + + Unused << aCookieService->GetCookieStringFromDocument(document, aCookie); +} + +// some #defines for comparison rules +#define MUST_BE_NULL 0 +#define MUST_EQUAL 1 +#define MUST_CONTAIN 2 +#define MUST_NOT_CONTAIN 3 +#define MUST_NOT_EQUAL 4 + +// a simple helper function to improve readability: +// takes one of the #defined rules above, and performs the appropriate test. +// true means the test passed; false means the test failed. +static inline bool CheckResult(const char* aLhs, uint32_t aRule, + const char* aRhs = nullptr) { + switch (aRule) { + case MUST_BE_NULL: + return !aLhs || !*aLhs; + + case MUST_EQUAL: + return !PL_strcmp(aLhs, aRhs); + + case MUST_NOT_EQUAL: + return PL_strcmp(aLhs, aRhs); + + case MUST_CONTAIN: + return strstr(aLhs, aRhs) != nullptr; + + case MUST_NOT_CONTAIN: + return strstr(aLhs, aRhs) == nullptr; + + default: + return false; // failure + } +} + +void InitPrefs(nsIPrefBranch* aPrefBranch) { + // init some relevant prefs, so the tests don't go awry. + // we use the most restrictive set of prefs we can; + // however, we don't test third party blocking here. + aPrefBranch->SetIntPref(kCookiesPermissions, 0); // accept all + // Set quotaPerHost to maxPerHost - 1, so there is only one cookie + // will be evicted everytime. + aPrefBranch->SetIntPref(kPrefCookieQuotaPerHost, 49); + // Set the base domain limit to 50 so we have a known value. + aPrefBranch->SetIntPref(kCookiesMaxPerHost, 50); + + // SameSite=None by default. We have other tests for lax-by-default. + // XXX: Bug 1617611 - Fix all the tests broken by "cookies SameSite=Lax by + // default" + Preferences::SetBool("network.cookie.sameSite.laxByDefault", false); + Preferences::SetBool("network.cookieJarSettings.unblocked_for_testing", true); + Preferences::SetBool("dom.securecontext.allowlist_onions", false); + Preferences::SetBool("network.cookie.sameSite.schemeful", false); +} + +TEST(TestCookie, TestCookieMain) +{ + nsresult rv0; + + nsCOMPtr<nsICookieService> cookieService = + do_GetService(kCookieServiceCID, &rv0); + ASSERT_NS_SUCCEEDED(rv0); + + nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(kPrefServiceCID, &rv0); + ASSERT_NS_SUCCEEDED(rv0); + + InitPrefs(prefBranch); + + nsCString cookie; + + /* The basic idea behind these tests is the following: + * + * we set() some cookie, then try to get() it in various ways. we have + * several possible tests we perform on the cookie string returned from + * get(): + * + * a) check whether the returned string is null (i.e. we got no cookies + * back). this is used e.g. to ensure a given cookie was deleted + * correctly, or to ensure a certain cookie wasn't returned to a given + * host. + * b) check whether the returned string exactly matches a given string. + * this is used where we want to make sure our cookie service adheres to + * some strict spec (e.g. ordering of multiple cookies), or where we + * just know exactly what the returned string should be. + * c) check whether the returned string contains/does not contain a given + * string. this is used where we don't know/don't care about the + * ordering of multiple cookies - we just want to make sure the cookie + * string contains them all, in some order. + * + * NOTE: this testsuite is not yet comprehensive or complete, and is + * somewhat contrived - still under development, and needs improving! + */ + + // test some basic variations of the domain & path + SetACookie(cookieService, "http://www.basic.com", "test=basic"); + GetACookie(cookieService, "http://www.basic.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=basic")); + GetACookie(cookieService, "http://www.basic.com/testPath/testfile.txt", + cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=basic")); + GetACookie(cookieService, "http://www.basic.com./", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + GetACookie(cookieService, "http://www.basic.com.", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + GetACookie(cookieService, "http://www.basic.com./testPath/testfile.txt", + cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + GetACookie(cookieService, "http://www.basic2.com/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + SetACookie(cookieService, "http://www.basic.com", "test=basic; max-age=-1"); + GetACookie(cookieService, "http://www.basic.com/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + // *** domain tests + + // test some variations of the domain & path, for different domains of + // a domain cookie + SetACookie(cookieService, "http://www.domain.com", + "test=domain; domain=domain.com"); + GetACookie(cookieService, "http://domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=domain")); + GetACookie(cookieService, "http://domain.com.", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + GetACookie(cookieService, "http://www.domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=domain")); + GetACookie(cookieService, "http://foo.domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=domain")); + SetACookie(cookieService, "http://www.domain.com", + "test=domain; domain=domain.com; max-age=-1"); + GetACookie(cookieService, "http://domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://www.domain.com", + "test=domain; domain=.domain.com"); + GetACookie(cookieService, "http://domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=domain")); + GetACookie(cookieService, "http://www.domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=domain")); + GetACookie(cookieService, "http://bah.domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=domain")); + SetACookie(cookieService, "http://www.domain.com", + "test=domain; domain=.domain.com; max-age=-1"); + GetACookie(cookieService, "http://domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://www.domain.com", + "test=domain; domain=.foo.domain.com"); + GetACookie(cookieService, "http://foo.domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://www.domain.com", + "test=domain; domain=moose.com"); + GetACookie(cookieService, "http://foo.domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://www.domain.com", + "test=domain; domain=domain.com."); + GetACookie(cookieService, "http://foo.domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://www.domain.com", + "test=domain; domain=..domain.com"); + GetACookie(cookieService, "http://foo.domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://www.domain.com", + "test=domain; domain=..domain.com."); + GetACookie(cookieService, "http://foo.domain.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://path.net/path/file", + R"(test=taco; path="/bogus")"); + GetACookie(cookieService, "http://path.net/path/file", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=taco")); + SetACookie(cookieService, "http://path.net/path/file", + "test=taco; max-age=-1"); + GetACookie(cookieService, "http://path.net/path/file", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + // *** path tests + + // test some variations of the domain & path, for different paths of + // a path cookie + SetACookie(cookieService, "http://path.net/path/file", + "test=path; path=/path"); + GetACookie(cookieService, "http://path.net/path", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=path")); + GetACookie(cookieService, "http://path.net/path/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=path")); + GetACookie(cookieService, "http://path.net/path/hithere.foo", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=path")); + GetACookie(cookieService, "http://path.net/path?hithere/foo", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=path")); + GetACookie(cookieService, "http://path.net/path2", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + GetACookie(cookieService, "http://path.net/path2/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + SetACookie(cookieService, "http://path.net/path/file", + "test=path; path=/path; max-age=-1"); + GetACookie(cookieService, "http://path.net/path/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://path.net/path/file", + "test=path; path=/path/"); + GetACookie(cookieService, "http://path.net/path", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + GetACookie(cookieService, "http://path.net/path/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=path")); + SetACookie(cookieService, "http://path.net/path/file", + "test=path; path=/path/; max-age=-1"); + GetACookie(cookieService, "http://path.net/path/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + // note that a site can set a cookie for a path it's not on. + // this is an intentional deviation from spec (see comments in + // CookieService::CheckPath()), so we test this functionality too + SetACookie(cookieService, "http://path.net/path/file", + "test=path; path=/foo/"); + GetACookie(cookieService, "http://path.net/path", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + GetACookie(cookieService, "http://path.net/foo", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + SetACookie(cookieService, "http://path.net/path/file", + "test=path; path=/foo/; max-age=-1"); + GetACookie(cookieService, "http://path.net/foo/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + // bug 373228: make sure cookies with paths longer than 1024 bytes, + // and cookies with paths or names containing tabs, are rejected. + // the following cookie has a path > 1024 bytes explicitly specified in the + // cookie + SetACookie( + cookieService, "http://path.net/", + "test=path; " + "path=/" + "123456789012345678901234567890123456789012345678901234567890123456789012" + "345678901234567890123456789012345678901234567890123456789012345678901234" + "567890123456789012345678901234567890123456789012345678901234567890123456" + "789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890123456789012345678901234567890123456789012345678901234567890" + "123456789012345678901234567890123456789012345678901234567890123456789012" + "345678901234567890123456789012345678901234567890123456789012345678901234" + "567890123456789012345678901234567890123456789012345678901234567890123456" + "789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890123456789012345678901234567890123456789012345678901234567890" + "123456789012345678901234567890123456789012345678901234567890123456789012" + "345678901234567890123456789012345678901234567890123456789012345678901234" + "567890123456789012345678901234567890123456789012345678901234567890123456" + "789012345678901234567890123456789012345678901234567890123456789012345678" + "9012345678901234567890/"); + GetACookie( + cookieService, + "http://path.net/" + "123456789012345678901234567890123456789012345678901234567890123456789012" + "345678901234567890123456789012345678901234567890123456789012345678901234" + "567890123456789012345678901234567890123456789012345678901234567890123456" + "789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890123456789012345678901234567890123456789012345678901234567890" + "123456789012345678901234567890123456789012345678901234567890123456789012" + "345678901234567890123456789012345678901234567890123456789012345678901234" + "567890123456789012345678901234567890123456789012345678901234567890123456" + "789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890123456789012345678901234567890123456789012345678901234567890" + "123456789012345678901234567890123456789012345678901234567890123456789012" + "345678901234567890123456789012345678901234567890123456789012345678901234" + "567890123456789012345678901234567890123456789012345678901234567890123456" + "789012345678901234567890123456789012345678901234567890123456789012345678" + "9012345678901234567890", + cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + // the following cookie has a path > 1024 bytes implicitly specified by the + // uri path + SetACookie( + cookieService, + "http://path.net/" + "123456789012345678901234567890123456789012345678901234567890123456789012" + "345678901234567890123456789012345678901234567890123456789012345678901234" + "567890123456789012345678901234567890123456789012345678901234567890123456" + "789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890123456789012345678901234567890123456789012345678901234567890" + "123456789012345678901234567890123456789012345678901234567890123456789012" + "345678901234567890123456789012345678901234567890123456789012345678901234" + "567890123456789012345678901234567890123456789012345678901234567890123456" + "789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890123456789012345678901234567890123456789012345678901234567890" + "123456789012345678901234567890123456789012345678901234567890123456789012" + "345678901234567890123456789012345678901234567890123456789012345678901234" + "567890123456789012345678901234567890123456789012345678901234567890123456" + "789012345678901234567890123456789012345678901234567890123456789012345678" + "9012345678901234567890/", + "test=path"); + GetACookie( + cookieService, + "http://path.net/" + "123456789012345678901234567890123456789012345678901234567890123456789012" + "345678901234567890123456789012345678901234567890123456789012345678901234" + "567890123456789012345678901234567890123456789012345678901234567890123456" + "789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890123456789012345678901234567890123456789012345678901234567890" + "123456789012345678901234567890123456789012345678901234567890123456789012" + "345678901234567890123456789012345678901234567890123456789012345678901234" + "567890123456789012345678901234567890123456789012345678901234567890123456" + "789012345678901234567890123456789012345678901234567890123456789012345678" + "901234567890123456789012345678901234567890123456789012345678901234567890" + "123456789012345678901234567890123456789012345678901234567890123456789012" + "345678901234567890123456789012345678901234567890123456789012345678901234" + "567890123456789012345678901234567890123456789012345678901234567890123456" + "789012345678901234567890123456789012345678901234567890123456789012345678" + "9012345678901234567890/", + cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + // the following cookie includes a tab in the path + SetACookie(cookieService, "http://path.net/", "test=path; path=/foo\tbar/"); + GetACookie(cookieService, "http://path.net/foo\tbar/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + // the following cookie includes a tab in the name + SetACookie(cookieService, "http://path.net/", "test\ttabs=tab"); + GetACookie(cookieService, "http://path.net/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + // the following cookie includes a tab in the value - allowed + SetACookie(cookieService, "http://path.net/", "test=tab\ttest"); + GetACookie(cookieService, "http://path.net/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=tab\ttest")); + SetACookie(cookieService, "http://path.net/", "test=tab\ttest; max-age=-1"); + GetACookie(cookieService, "http://path.net/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + // *** expiry & deletion tests + // XXX add server time str parsing tests here + + // test some variations of the expiry time, + // and test deletion of previously set cookies + SetACookie(cookieService, "http://expireme.org/", "test=expiry; max-age=-1"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + SetACookie(cookieService, "http://expireme.org/", "test=expiry; max-age=0"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + SetACookie(cookieService, "http://expireme.org/", "test=expiry; expires=bad"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=expiry")); + SetACookie(cookieService, "http://expireme.org/", + "test=expiry; expires=Thu, 10 Apr 1980 16:33:12 GMT"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + SetACookie(cookieService, "http://expireme.org/", + R"(test=expiry; expires="Thu, 10 Apr 1980 16:33:12 GMT)"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + SetACookie(cookieService, "http://expireme.org/", + R"(test=expiry; expires="Thu, 10 Apr 1980 16:33:12 GMT")"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://expireme.org/", "test=expiry; max-age=60"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=expiry")); + SetACookie(cookieService, "http://expireme.org/", "test=expiry; max-age=-20"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + SetACookie(cookieService, "http://expireme.org/", "test=expiry; max-age=60"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=expiry")); + SetACookie(cookieService, "http://expireme.org/", + "test=expiry; expires=Thu, 10 Apr 1980 16:33:12 GMT"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + SetACookie(cookieService, "http://expireme.org/", "test=expiry; max-age=60"); + SetACookie(cookieService, "http://expireme.org/", + "newtest=expiry; max-age=60"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, "test=expiry")); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, "newtest=expiry")); + SetACookie(cookieService, "http://expireme.org/", + "test=differentvalue; max-age=0"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "newtest=expiry")); + SetACookie(cookieService, "http://expireme.org/", + "newtest=evendifferentvalue; max-age=0"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://foo.expireme.org/", + "test=expiry; domain=.expireme.org; max-age=60"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=expiry")); + SetACookie(cookieService, "http://bar.expireme.org/", + "test=differentvalue; domain=.expireme.org; max-age=0"); + GetACookie(cookieService, "http://expireme.org/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + nsAutoCString ServerTime; + nsAutoCString CookieString; + + // *** multiple cookie tests + + // test the setting of multiple cookies, and test the order of precedence + // (a later cookie overwriting an earlier one, in the same header string) + SetACookie(cookieService, "http://multiple.cookies/", + "test=multiple; domain=.multiple.cookies \n test=different \n " + "test=same; domain=.multiple.cookies \n newtest=ciao \n " + "newtest=foo; max-age=-6 \n newtest=reincarnated"); + GetACookie(cookieService, "http://multiple.cookies/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_NOT_CONTAIN, "test=multiple")); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, "test=different")); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, "test=same")); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_NOT_CONTAIN, "newtest=ciao")); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_NOT_CONTAIN, "newtest=foo")); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, "newtest=reincarnated")); + SetACookie(cookieService, "http://multiple.cookies/", + "test=expiry; domain=.multiple.cookies; max-age=0"); + GetACookie(cookieService, "http://multiple.cookies/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_NOT_CONTAIN, "test=same")); + SetACookie(cookieService, "http://multiple.cookies/", + "\n test=different; max-age=0 \n"); + GetACookie(cookieService, "http://multiple.cookies/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_NOT_CONTAIN, "test=different")); + SetACookie(cookieService, "http://multiple.cookies/", + "newtest=dead; max-age=0"); + GetACookie(cookieService, "http://multiple.cookies/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + // *** parser tests + + // test the cookie header parser, under various circumstances. + SetACookie(cookieService, "http://parser.test/", + "test=parser; domain=.parser.test; ;; ;=; ,,, ===,abc,=; " + "abracadabra! max-age=20;=;;"); + GetACookie(cookieService, "http://parser.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=parser")); + SetACookie(cookieService, "http://parser.test/", + "test=parser; domain=.parser.test; max-age=0"); + GetACookie(cookieService, "http://parser.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + SetACookie(cookieService, "http://parser.test/", + "test=\"fubar! = foo;bar\\\";\" parser; domain=.parser.test; " + "max-age=6\nfive; max-age=2.63,"); + GetACookie(cookieService, "http://parser.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, R"(test="fubar! = foo)")); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, "five")); + SetACookie(cookieService, "http://parser.test/", + "test=kill; domain=.parser.test; max-age=0 \n five; max-age=0"); + GetACookie(cookieService, "http://parser.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + // test the handling of VALUE-only cookies (see bug 169091), + // i.e. "six" should assume an empty NAME, which allows other VALUE-only + // cookies to overwrite it + SetACookie(cookieService, "http://parser.test/", "six"); + GetACookie(cookieService, "http://parser.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "six")); + SetACookie(cookieService, "http://parser.test/", "seven"); + GetACookie(cookieService, "http://parser.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "seven")); + SetACookie(cookieService, "http://parser.test/", " =eight"); + GetACookie(cookieService, "http://parser.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "eight")); + SetACookie(cookieService, "http://parser.test/", "test=six"); + GetACookie(cookieService, "http://parser.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, "test=six")); + + // *** path ordering tests + + // test that cookies are returned in path order - longest to shortest. + // if the header doesn't specify a path, it's taken from the host URI. + SetACookie(cookieService, "http://multi.path.tests/", + "test1=path; path=/one/two/three"); + SetACookie(cookieService, "http://multi.path.tests/", + "test2=path; path=/one \n test3=path; path=/one/two/three/four \n " + "test4=path; path=/one/two \n test5=path; path=/one/two/"); + SetACookie(cookieService, "http://multi.path.tests/one/two/three/four/five/", + "test6=path"); + SetACookie(cookieService, + "http://multi.path.tests/one/two/three/four/five/six/", + "test7=path; path="); + SetACookie(cookieService, "http://multi.path.tests/", "test8=path; path=/"); + GetACookie(cookieService, + "http://multi.path.tests/one/two/three/four/five/six/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, + "test7=path; test6=path; test3=path; test1=path; " + "test5=path; test4=path; test2=path; test8=path")); + + // *** Cookie prefix tests + + // prefixed cookies can't be set from insecure HTTP + SetACookie(cookieService, "http://prefixed.test/", "__Secure-test1=test"); + SetACookie(cookieService, "http://prefixed.test/", + "__Secure-test2=test; secure"); + SetACookie(cookieService, "http://prefixed.test/", "__Host-test1=test"); + SetACookie(cookieService, "http://prefixed.test/", + "__Host-test2=test; secure"); + GetACookie(cookieService, "http://prefixed.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + // prefixed cookies won't be set without the secure flag + SetACookie(cookieService, "https://prefixed.test/", "__Secure-test=test"); + SetACookie(cookieService, "https://prefixed.test/", "__Host-test=test"); + GetACookie(cookieService, "https://prefixed.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + // prefixed cookies can be set when done correctly + SetACookie(cookieService, "https://prefixed.test/", + "__Secure-test=test; secure"); + SetACookie(cookieService, "https://prefixed.test/", + "__Host-test=test; secure"); + GetACookie(cookieService, "https://prefixed.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, "__Secure-test=test")); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, "__Host-test=test")); + + // but when set must not be returned to the host insecurely + GetACookie(cookieService, "http://prefixed.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + // Host-prefixed cookies cannot specify a domain + SetACookie(cookieService, "https://host.prefixed.test/", + "__Host-a=test; secure; domain=prefixed.test"); + SetACookie(cookieService, "https://host.prefixed.test/", + "__Host-b=test; secure; domain=.prefixed.test"); + SetACookie(cookieService, "https://host.prefixed.test/", + "__Host-c=test; secure; domain=host.prefixed.test"); + SetACookie(cookieService, "https://host.prefixed.test/", + "__Host-d=test; secure; domain=.host.prefixed.test"); + GetACookie(cookieService, "https://host.prefixed.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + // Host-prefixed cookies can only have a path of "/" + SetACookie(cookieService, "https://host.prefixed.test/some/path", + "__Host-e=test; secure"); + SetACookie(cookieService, "https://host.prefixed.test/some/path", + "__Host-f=test; secure; path=/"); + SetACookie(cookieService, "https://host.prefixed.test/some/path", + "__Host-g=test; secure; path=/some"); + GetACookie(cookieService, "https://host.prefixed.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "__Host-f=test")); + + // *** leave-secure-alone tests + + // testing items 0 & 1 for 3.1 of spec Deprecate modification of ’secure’ + // cookies from non-secure origins + SetACookie(cookieService, "http://www.security.test/", + "test=non-security; secure"); + GetACookieNoHttp(cookieService, "https://www.security.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + SetACookie(cookieService, "https://www.security.test/path/", + "test=security; secure; path=/path/"); + GetACookieNoHttp(cookieService, "https://www.security.test/path/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=security")); + // testing items 2 & 3 & 4 for 3.2 of spec Deprecate modification of ’secure’ + // cookies from non-secure origins + // Secure site can modify cookie value + SetACookie(cookieService, "https://www.security.test/path/", + "test=security2; secure; path=/path/"); + GetACookieNoHttp(cookieService, "https://www.security.test/path/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=security2")); + // If new cookie contains same name, same host and partially matching path + // with an existing security cookie on non-security site, it can't modify an + // existing security cookie. + SetACookie(cookieService, "http://www.security.test/path/foo/", + "test=non-security; path=/path/foo"); + GetACookieNoHttp(cookieService, "https://www.security.test/path/foo/", + cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=security2")); + // Non-secure cookie can set by same name, same host and non-matching path. + SetACookie(cookieService, "http://www.security.test/bar/", + "test=non-security; path=/bar"); + GetACookieNoHttp(cookieService, "http://www.security.test/bar/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=non-security")); + // Modify value and downgrade secure level. + SetACookie( + cookieService, "https://www.security.test/", + "test_modify_cookie=security-cookie; secure; domain=.security.test"); + GetACookieNoHttp(cookieService, "https://www.security.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, + "test_modify_cookie=security-cookie")); + SetACookie(cookieService, "https://www.security.test/", + "test_modify_cookie=non-security-cookie; domain=.security.test"); + GetACookieNoHttp(cookieService, "https://www.security.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, + "test_modify_cookie=non-security-cookie")); + + // Test the non-security cookie can set when domain or path not same to secure + // cookie of same name. + SetACookie(cookieService, "https://www.security.test/", "test=security3"); + GetACookieNoHttp(cookieService, "http://www.security.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, "test=security3")); + SetACookie(cookieService, "http://www.security.test/", + "test=non-security2; domain=security.test"); + GetACookieNoHttp(cookieService, "http://www.security.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, "test=non-security2")); + + // *** nsICookieManager interface tests + nsCOMPtr<nsICookieManager> cookieMgr = + do_GetService(NS_COOKIEMANAGER_CONTRACTID, &rv0); + ASSERT_NS_SUCCEEDED(rv0); + + const nsCOMPtr<nsICookieManager>& cookieMgr2 = cookieMgr; + ASSERT_TRUE(cookieMgr2); + + mozilla::OriginAttributes attrs; + + // first, ensure a clean slate + EXPECT_NS_SUCCEEDED(cookieMgr->RemoveAll()); + // add some cookies + EXPECT_TRUE(NS_SUCCEEDED(cookieMgr2->AddNative("cookiemgr.test"_ns, // domain + "/foo"_ns, // path + "test1"_ns, // name + "yes"_ns, // value + false, // is secure + false, // is httponly + true, // is session + INT64_MAX, // expiry time + &attrs, // originAttributes + nsICookie::SAMESITE_NONE, + nsICookie::SCHEME_HTTPS))); + EXPECT_TRUE(NS_SUCCEEDED(cookieMgr2->AddNative( + "cookiemgr.test"_ns, // domain + "/foo"_ns, // path + "test2"_ns, // name + "yes"_ns, // value + false, // is secure + true, // is httponly + true, // is session + PR_Now() / PR_USEC_PER_SEC + 2, // expiry time + &attrs, // originAttributes + nsICookie::SAMESITE_NONE, nsICookie::SCHEME_HTTPS))); + EXPECT_TRUE(NS_SUCCEEDED(cookieMgr2->AddNative("new.domain"_ns, // domain + "/rabbit"_ns, // path + "test3"_ns, // name + "yes"_ns, // value + false, // is secure + false, // is httponly + true, // is session + INT64_MAX, // expiry time + &attrs, // originAttributes + nsICookie::SAMESITE_NONE, + nsICookie::SCHEME_HTTPS))); + // confirm using enumerator + nsTArray<RefPtr<nsICookie>> cookies; + EXPECT_NS_SUCCEEDED(cookieMgr->GetCookies(cookies)); + nsCOMPtr<nsICookie> expiredCookie, newDomainCookie; + for (const auto& cookie : cookies) { + nsAutoCString name; + cookie->GetName(name); + if (name.EqualsLiteral("test2")) { + expiredCookie = cookie; + } else if (name.EqualsLiteral("test3")) { + newDomainCookie = cookie; + } + } + EXPECT_EQ(cookies.Length(), 3ul); + // check the httpOnly attribute of the second cookie is honored + GetACookie(cookieService, "http://cookiemgr.test/foo/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_CONTAIN, "test2=yes")); + GetACookieNoHttp(cookieService, "http://cookiemgr.test/foo/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_NOT_CONTAIN, "test2=yes")); + // check CountCookiesFromHost() + uint32_t hostCookies = 0; + EXPECT_TRUE(NS_SUCCEEDED( + cookieMgr2->CountCookiesFromHost("cookiemgr.test"_ns, &hostCookies))); + EXPECT_EQ(hostCookies, 2u); + // check CookieExistsNative() using the third cookie + bool found; + EXPECT_TRUE(NS_SUCCEEDED(cookieMgr2->CookieExistsNative( + "new.domain"_ns, "/rabbit"_ns, "test3"_ns, &attrs, &found))); + EXPECT_TRUE(found); + + // sleep four seconds, to make sure the second cookie has expired + PR_Sleep(4 * PR_TicksPerSecond()); + // check that both CountCookiesFromHost() and CookieExistsNative() count the + // expired cookie + EXPECT_TRUE(NS_SUCCEEDED( + cookieMgr2->CountCookiesFromHost("cookiemgr.test"_ns, &hostCookies))); + EXPECT_EQ(hostCookies, 2u); + EXPECT_TRUE(NS_SUCCEEDED(cookieMgr2->CookieExistsNative( + "cookiemgr.test"_ns, "/foo"_ns, "test2"_ns, &attrs, &found))); + EXPECT_TRUE(found); + // double-check RemoveAll() using the enumerator + EXPECT_NS_SUCCEEDED(cookieMgr->RemoveAll()); + cookies.SetLength(0); + EXPECT_TRUE(NS_SUCCEEDED(cookieMgr->GetCookies(cookies)) && + cookies.IsEmpty()); + + // *** eviction and creation ordering tests + + // test that cookies are + // a) returned by order of creation time (oldest first, newest last) + // b) evicted by order of lastAccessed time, if the limit on cookies per host + // (50) is reached + nsAutoCString name; + nsAutoCString expected; + for (int32_t i = 0; i < 60; ++i) { + name = "test"_ns; + name.AppendInt(i); + name += "=creation"_ns; + SetACookie(cookieService, "http://creation.ordering.tests/", name.get()); + + if (i >= 10) { + expected += name; + if (i < 59) expected += "; "_ns; + } + } + GetACookie(cookieService, "http://creation.ordering.tests/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, expected.get())); + + cookieMgr->RemoveAll(); + + for (int32_t i = 0; i < 60; ++i) { + name = "test"_ns; + name.AppendInt(i); + name += "=delete_non_security"_ns; + + // Create 50 cookies that include the secure flag. + if (i < 50) { + name += "; secure"_ns; + SetACookie(cookieService, "https://creation.ordering.tests/", name.get()); + } else { + // non-security cookies will be removed beside the latest cookie that be + // created. + SetACookie(cookieService, "http://creation.ordering.tests/", name.get()); + } + } + GetACookie(cookieService, "http://creation.ordering.tests/", cookie); + + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + // *** SameSite attribute - parsing and cookie storage tests + // Clear the cookies + EXPECT_NS_SUCCEEDED(cookieMgr->RemoveAll()); + + // None of these cookies will be set because using + // CookieJarSettings::GetBlockingAll(). + SetACookieJarBlocked(cookieService, "http://samesite.test", "unset=yes"); + SetACookieJarBlocked(cookieService, "http://samesite.test", + "unspecified=yes; samesite"); + SetACookieJarBlocked(cookieService, "http://samesite.test", + "empty=yes; samesite="); + SetACookieJarBlocked(cookieService, "http://samesite.test", + "bogus=yes; samesite=bogus"); + SetACookieJarBlocked(cookieService, "http://samesite.test", + "strict=yes; samesite=strict"); + SetACookieJarBlocked(cookieService, "http://samesite.test", + "lax=yes; samesite=lax"); + + cookies.SetLength(0); + EXPECT_NS_SUCCEEDED(cookieMgr->GetCookies(cookies)); + + EXPECT_TRUE(cookies.IsEmpty()); + + // Set cookies with various incantations of the samesite attribute: + // No same site attribute present + SetACookie(cookieService, "http://samesite.test", "unset=yes"); + // samesite attribute present but with no value + SetACookie(cookieService, "http://samesite.test", + "unspecified=yes; samesite"); + // samesite attribute present but with an empty value + SetACookie(cookieService, "http://samesite.test", "empty=yes; samesite="); + // samesite attribute present but with an invalid value + SetACookie(cookieService, "http://samesite.test", + "bogus=yes; samesite=bogus"); + // samesite=strict + SetACookie(cookieService, "http://samesite.test", + "strict=yes; samesite=strict"); + // samesite=lax + SetACookie(cookieService, "http://samesite.test", "lax=yes; samesite=lax"); + + cookies.SetLength(0); + EXPECT_NS_SUCCEEDED(cookieMgr->GetCookies(cookies)); + + // check the cookies for the required samesite value + for (const auto& cookie : cookies) { + nsAutoCString name; + cookie->GetName(name); + int32_t sameSiteAttr; + cookie->GetSameSite(&sameSiteAttr); + if (name.EqualsLiteral("unset")) { + EXPECT_TRUE(sameSiteAttr == nsICookie::SAMESITE_NONE); + } else if (name.EqualsLiteral("unspecified")) { + EXPECT_TRUE(sameSiteAttr == nsICookie::SAMESITE_NONE); + } else if (name.EqualsLiteral("empty")) { + EXPECT_TRUE(sameSiteAttr == nsICookie::SAMESITE_NONE); + } else if (name.EqualsLiteral("bogus")) { + EXPECT_TRUE(sameSiteAttr == nsICookie::SAMESITE_NONE); + } else if (name.EqualsLiteral("strict")) { + EXPECT_TRUE(sameSiteAttr == nsICookie::SAMESITE_STRICT); + } else if (name.EqualsLiteral("lax")) { + EXPECT_TRUE(sameSiteAttr == nsICookie::SAMESITE_LAX); + } + } + + EXPECT_TRUE(cookies.Length() == 6); + + // *** SameSite attribute + // Clear the cookies + EXPECT_NS_SUCCEEDED(cookieMgr->RemoveAll()); + + // please note that the flag aForeign is always set to true using this test + // setup because no nsIChannel is passed to SetCookieString(). therefore we + // can only test that no cookies are sent for cross origin requests using + // same-site cookies. + SetACookie(cookieService, "http://www.samesite.com", + "test=sameSiteStrictVal; samesite=strict"); + GetACookie(cookieService, "http://www.notsamesite.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://www.samesite.test", + "test=sameSiteLaxVal; samesite=lax"); + GetACookie(cookieService, "http://www.notsamesite.com", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + static const char* secureURIs[] = { + "http://localhost", "http://localhost:1234", "http://127.0.0.1", + "http://127.0.0.2", "http://127.1.0.1", "http://[::1]", + // TODO bug 1220810 "http://xyzzy.localhost" + }; + + uint32_t numSecureURIs = sizeof(secureURIs) / sizeof(const char*); + for (uint32_t i = 0; i < numSecureURIs; ++i) { + SetACookie(cookieService, secureURIs[i], "test=basic; secure"); + GetACookie(cookieService, secureURIs[i], cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=basic")); + SetACookie(cookieService, secureURIs[i], "test=basic1"); + GetACookie(cookieService, secureURIs[i], cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=basic1")); + } + + // XXX the following are placeholders: add these tests please! + // *** "noncompliant cookie" tests + // *** IP address tests + // *** speed tests +} + +TEST(TestCookie, SameSiteLax) +{ + Preferences::SetBool("network.cookie.sameSite.laxByDefault", true); + + nsresult rv; + + nsCOMPtr<nsICookieService> cookieService = + do_GetService(kCookieServiceCID, &rv); + ASSERT_NS_SUCCEEDED(rv); + + nsCOMPtr<nsICookieManager> cookieMgr = + do_GetService(NS_COOKIEMANAGER_CONTRACTID, &rv); + ASSERT_NS_SUCCEEDED(rv); + + EXPECT_NS_SUCCEEDED(cookieMgr->RemoveAll()); + + SetACookie(cookieService, "http://samesite.test", "unset=yes"); + + nsTArray<RefPtr<nsICookie>> cookies; + EXPECT_NS_SUCCEEDED(cookieMgr->GetCookies(cookies)); + EXPECT_EQ(cookies.Length(), (uint64_t)1); + + Cookie* cookie = static_cast<Cookie*>(cookies[0].get()); + EXPECT_EQ(cookie->RawSameSite(), nsICookie::SAMESITE_NONE); + EXPECT_EQ(cookie->SameSite(), nsICookie::SAMESITE_LAX); + + Preferences::SetCString("network.cookie.sameSite.laxByDefault.disabledHosts", + "foo.com,samesite.test,bar.net"); + + EXPECT_NS_SUCCEEDED(cookieMgr->RemoveAll()); + + cookies.SetLength(0); + EXPECT_NS_SUCCEEDED(cookieMgr->GetCookies(cookies)); + EXPECT_EQ(cookies.Length(), (uint64_t)0); + + SetACookie(cookieService, "http://samesite.test", "unset=yes"); + + cookies.SetLength(0); + EXPECT_NS_SUCCEEDED(cookieMgr->GetCookies(cookies)); + EXPECT_EQ(cookies.Length(), (uint64_t)1); + + cookie = static_cast<Cookie*>(cookies[0].get()); + EXPECT_EQ(cookie->RawSameSite(), nsICookie::SAMESITE_NONE); + EXPECT_EQ(cookie->SameSite(), nsICookie::SAMESITE_LAX); +} + +TEST(TestCookie, OnionSite) +{ + Preferences::SetBool("dom.securecontext.allowlist_onions", true); + Preferences::SetBool("network.cookie.sameSite.laxByDefault", false); + + nsresult rv; + nsCString cookie; + + nsCOMPtr<nsICookieService> cookieService = + do_GetService(kCookieServiceCID, &rv); + ASSERT_NS_SUCCEEDED(rv); + + // .onion secure cookie tests + SetACookie(cookieService, "http://123456789abcdef.onion/", + "test=onion-security; secure"); + GetACookieNoHttp(cookieService, "https://123456789abcdef.onion/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=onion-security")); + SetACookie(cookieService, "http://123456789abcdef.onion/", + "test=onion-security2; secure"); + GetACookieNoHttp(cookieService, "http://123456789abcdef.onion/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=onion-security2")); + SetACookie(cookieService, "https://123456789abcdef.onion/", + "test=onion-security3; secure"); + GetACookieNoHttp(cookieService, "http://123456789abcdef.onion/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=onion-security3")); + SetACookie(cookieService, "http://123456789abcdef.onion/", + "test=onion-security4"); + GetACookieNoHttp(cookieService, "http://123456789abcdef.onion/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "test=onion-security4")); +} + +TEST(TestCookie, HiddenPrefix) +{ + nsresult rv; + nsCString cookie; + + nsCOMPtr<nsICookieService> cookieService = + do_GetService(kCookieServiceCID, &rv); + ASSERT_NS_SUCCEEDED(rv); + + SetACookie(cookieService, "http://hiddenprefix.test/", "=__Host-test=a"); + GetACookie(cookieService, "http://hiddenprefix.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://hiddenprefix.test/", "=__Secure-test=a"); + GetACookie(cookieService, "http://hiddenprefix.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://hiddenprefix.test/", "=__Host-check"); + GetACookie(cookieService, "http://hiddenprefix.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://hiddenprefix.test/", "=__Secure-check"); + GetACookie(cookieService, "http://hiddenprefix.test/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); +} + +TEST(TestCookie, BlockUnicode) +{ + Preferences::SetBool("network.cookie.blockUnicode", true); + + nsresult rv; + nsCString cookie; + + nsCOMPtr<nsICookieService> cookieService = + do_GetService(kCookieServiceCID, &rv); + ASSERT_NS_SUCCEEDED(rv); + + SetACookie(cookieService, "http://unicode.com/", "name=🍪"); + GetACookie(cookieService, "http://unicode.com/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + SetACookie(cookieService, "http://unicode.com/", "🍪=value"); + GetACookie(cookieService, "http://unicode.com/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_BE_NULL)); + + Preferences::SetBool("network.cookie.blockUnicode", false); + + SetACookie(cookieService, "http://unicode.com/", "name=🍪"); + GetACookie(cookieService, "http://unicode.com/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "name=🍪")); + + nsCOMPtr<nsICookieManager> cookieMgr = + do_GetService(NS_COOKIEMANAGER_CONTRACTID); + EXPECT_NS_SUCCEEDED(cookieMgr->RemoveAll()); + + SetACookie(cookieService, "http://unicode.com/", "🍪=value"); + GetACookie(cookieService, "http://unicode.com/", cookie); + EXPECT_TRUE(CheckResult(cookie.get(), MUST_EQUAL, "🍪=value")); + + EXPECT_NS_SUCCEEDED(cookieMgr->RemoveAll()); + Preferences::ClearUser("network.cookie.blockUnicode"); +} diff --git a/netwerk/test/gtest/TestDNSPacket.cpp b/netwerk/test/gtest/TestDNSPacket.cpp new file mode 100644 index 0000000000..49530b80e0 --- /dev/null +++ b/netwerk/test/gtest/TestDNSPacket.cpp @@ -0,0 +1,69 @@ +#include "gtest/gtest.h" + +#include "mozilla/net/DNSPacket.h" +#include "mozilla/Preferences.h" + +using namespace mozilla; +using namespace mozilla::net; + +void AssertDnsPadding(uint32_t PaddingLength, unsigned int WithPadding, + unsigned int WithoutPadding, bool DisableEcn, + const nsCString& host) { + DNSPacket encoder; + nsCString buf; + + ASSERT_EQ(Preferences::SetUint("network.trr.padding.length", PaddingLength), + NS_OK); + + ASSERT_EQ(Preferences::SetBool("network.trr.padding", true), NS_OK); + ASSERT_EQ(encoder.EncodeRequest(buf, host, 1, DisableEcn), NS_OK); + ASSERT_EQ(buf.Length(), WithPadding); + + ASSERT_EQ(Preferences::SetBool("network.trr.padding", false), NS_OK); + ASSERT_EQ(encoder.EncodeRequest(buf, host, 1, DisableEcn), NS_OK); + ASSERT_EQ(buf.Length(), WithoutPadding); +} + +TEST(TestDNSPacket, PaddingLenEcn) +{ + AssertDnsPadding(16, 48, 41, true, "a.de"_ns); + AssertDnsPadding(16, 48, 42, true, "ab.de"_ns); + AssertDnsPadding(16, 48, 43, true, "abc.de"_ns); + AssertDnsPadding(16, 48, 44, true, "abcd.de"_ns); + AssertDnsPadding(16, 64, 45, true, "abcde.de"_ns); + AssertDnsPadding(16, 64, 46, true, "abcdef.de"_ns); + AssertDnsPadding(16, 64, 47, true, "abcdefg.de"_ns); + AssertDnsPadding(16, 64, 48, true, "abcdefgh.de"_ns); +} + +TEST(TestDNSPacket, PaddingLenDisableEcn) +{ + AssertDnsPadding(16, 48, 22, false, "a.de"_ns); + AssertDnsPadding(16, 48, 23, false, "ab.de"_ns); + AssertDnsPadding(16, 48, 24, false, "abc.de"_ns); + AssertDnsPadding(16, 48, 25, false, "abcd.de"_ns); + AssertDnsPadding(16, 48, 26, false, "abcde.de"_ns); + AssertDnsPadding(16, 48, 27, false, "abcdef.de"_ns); + AssertDnsPadding(16, 48, 32, false, "abcdefghijk.de"_ns); + AssertDnsPadding(16, 48, 33, false, "abcdefghijkl.de"_ns); + AssertDnsPadding(16, 64, 34, false, "abcdefghijklm.de"_ns); + AssertDnsPadding(16, 64, 35, false, "abcdefghijklmn.de"_ns); +} + +TEST(TestDNSPacket, PaddingLengths) +{ + AssertDnsPadding(0, 45, 41, true, "a.de"_ns); + AssertDnsPadding(1, 45, 41, true, "a.de"_ns); + AssertDnsPadding(2, 46, 41, true, "a.de"_ns); + AssertDnsPadding(3, 45, 41, true, "a.de"_ns); + AssertDnsPadding(4, 48, 41, true, "a.de"_ns); + AssertDnsPadding(16, 48, 41, true, "a.de"_ns); + AssertDnsPadding(32, 64, 41, true, "a.de"_ns); + AssertDnsPadding(42, 84, 41, true, "a.de"_ns); + AssertDnsPadding(52, 52, 41, true, "a.de"_ns); + AssertDnsPadding(80, 80, 41, true, "a.de"_ns); + AssertDnsPadding(128, 128, 41, true, "a.de"_ns); + AssertDnsPadding(256, 256, 41, true, "a.de"_ns); + AssertDnsPadding(1024, 1024, 41, true, "a.de"_ns); + AssertDnsPadding(1025, 1024, 41, true, "a.de"_ns); +} diff --git a/netwerk/test/gtest/TestHeaders.cpp b/netwerk/test/gtest/TestHeaders.cpp new file mode 100644 index 0000000000..0da6b06c70 --- /dev/null +++ b/netwerk/test/gtest/TestHeaders.cpp @@ -0,0 +1,29 @@ +#include "gtest/gtest.h" + +#include "nsHttpHeaderArray.h" + +TEST(TestHeaders, DuplicateHSTS) +{ + // When the Strict-Transport-Security header is sent multiple times, its + // effective value is the value of the first item. It is not merged as other + // headers are. + mozilla::net::nsHttpHeaderArray headers; + nsresult rv = headers.SetHeaderFromNet( + mozilla::net::nsHttp::Strict_Transport_Security, + "Strict_Transport_Security"_ns, "max-age=360"_ns, true); + ASSERT_EQ(rv, NS_OK); + + nsAutoCString h; + rv = headers.GetHeader(mozilla::net::nsHttp::Strict_Transport_Security, h); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(h.get(), "max-age=360"); + + rv = headers.SetHeaderFromNet(mozilla::net::nsHttp::Strict_Transport_Security, + "Strict_Transport_Security"_ns, + "max-age=720"_ns, true); + ASSERT_EQ(rv, NS_OK); + + rv = headers.GetHeader(mozilla::net::nsHttp::Strict_Transport_Security, h); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(h.get(), "max-age=360"); +} diff --git a/netwerk/test/gtest/TestHttpAuthUtils.cpp b/netwerk/test/gtest/TestHttpAuthUtils.cpp new file mode 100644 index 0000000000..78fb40d4d0 --- /dev/null +++ b/netwerk/test/gtest/TestHttpAuthUtils.cpp @@ -0,0 +1,43 @@ +#include "gtest/gtest.h" + +#include "mozilla/net/HttpAuthUtils.h" +#include "mozilla/Preferences.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace net { + +#define TEST_PREF "network.http_test.auth_utils" + +TEST(TestHttpAuthUtils, Bug1351301) +{ + nsCOMPtr<nsIURI> url; + nsAutoCString spec; + + ASSERT_EQ(Preferences::SetCString(TEST_PREF, "bar.com"), NS_OK); + spec = "http://bar.com"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(auth::URIMatchesPrefPattern(url, TEST_PREF), true); + + spec = "http://foo.bar.com"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(auth::URIMatchesPrefPattern(url, TEST_PREF), true); + + spec = "http://foobar.com"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(auth::URIMatchesPrefPattern(url, TEST_PREF), false); + + ASSERT_EQ(Preferences::SetCString(TEST_PREF, ".bar.com"), NS_OK); + spec = "http://foo.bar.com"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(auth::URIMatchesPrefPattern(url, TEST_PREF), true); + + spec = "http://bar.com"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(auth::URIMatchesPrefPattern(url, TEST_PREF), false); + + ASSERT_EQ(Preferences::ClearUser(TEST_PREF), NS_OK); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/test/gtest/TestHttpChannel.cpp b/netwerk/test/gtest/TestHttpChannel.cpp new file mode 100644 index 0000000000..10ef744bb4 --- /dev/null +++ b/netwerk/test/gtest/TestHttpChannel.cpp @@ -0,0 +1,135 @@ +#include "gtest/gtest.h" + +#include "nsCOMPtr.h" +#include "mozilla/Maybe.h" +#include "mozilla/PreloadHashKey.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" +#include "nsIStreamListener.h" +#include "nsThreadUtils.h" +#include "nsStringStream.h" +#include "nsIPrivateBrowsingChannel.h" +#include "nsIInterfaceRequestor.h" +#include "nsContentUtils.h" + +using namespace mozilla; + +class FakeListener : public nsIStreamListener, public nsIInterfaceRequestor { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + + enum { Never, OnStart, OnData, OnStop } mCancelIn = Never; + + nsresult mOnStartResult = NS_OK; + nsresult mOnDataResult = NS_OK; + nsresult mOnStopResult = NS_OK; + + bool mOnStart = false; + nsCString mOnData; + Maybe<nsresult> mOnStop; + + private: + virtual ~FakeListener() = default; +}; + +NS_IMPL_ISUPPORTS(FakeListener, nsIStreamListener, nsIRequestObserver, + nsIInterfaceRequestor) + +NS_IMETHODIMP +FakeListener::GetInterface(const nsIID& aIID, void** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = nullptr; + return NS_NOINTERFACE; +} + +NS_IMETHODIMP FakeListener::OnStartRequest(nsIRequest* request) { + EXPECT_FALSE(mOnStart); + mOnStart = true; + + if (mCancelIn == OnStart) { + request->Cancel(NS_ERROR_ABORT); + } + + return mOnStartResult; +} + +NS_IMETHODIMP FakeListener::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, + uint64_t offset, uint32_t count) { + nsAutoCString data; + data.SetLength(count); + + uint32_t read; + input->Read(data.BeginWriting(), count, &read); + mOnData += data; + + if (mCancelIn == OnData) { + request->Cancel(NS_ERROR_ABORT); + } + + return mOnDataResult; +} + +NS_IMETHODIMP FakeListener::OnStopRequest(nsIRequest* request, + nsresult status) { + EXPECT_FALSE(mOnStop); + mOnStop.emplace(status); + + if (mCancelIn == OnStop) { + request->Cancel(NS_ERROR_ABORT); + } + + return mOnStopResult; +} + +// Test that nsHttpChannel::AsyncOpen properly picks up changes to +// loadInfo.mPrivateBrowsingId that occur after the channel was created. +TEST(TestHttpChannel, PBAsyncOpen) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "http://localhost/"_ns); + + nsCOMPtr<nsIChannel> channel; + nsresult rv = NS_NewChannel( + getter_AddRefs(channel), uri, nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + ASSERT_EQ(rv, NS_OK); + + RefPtr<FakeListener> listener = new FakeListener(); + rv = channel->SetNotificationCallbacks(listener); + ASSERT_EQ(rv, NS_OK); + + nsCOMPtr<nsIPrivateBrowsingChannel> pbchannel = do_QueryInterface(channel); + ASSERT_TRUE(pbchannel); + + bool isPrivate = false; + rv = pbchannel->GetIsChannelPrivate(&isPrivate); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(isPrivate, false); + + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + OriginAttributes attrs; + attrs.mPrivateBrowsingId = 1; + rv = loadInfo->SetOriginAttributes(attrs); + ASSERT_EQ(rv, NS_OK); + + rv = pbchannel->GetIsChannelPrivate(&isPrivate); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(isPrivate, false); + + rv = channel->AsyncOpen(listener); + ASSERT_EQ(rv, NS_OK); + + rv = pbchannel->GetIsChannelPrivate(&isPrivate); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(isPrivate, true); + + MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil( + "TEST(TestHttpChannel, PBAsyncOpen)"_ns, + [&]() -> bool { return listener->mOnStop.isSome(); })); +} diff --git a/netwerk/test/gtest/TestHttpResponseHead.cpp b/netwerk/test/gtest/TestHttpResponseHead.cpp new file mode 100644 index 0000000000..2d2c557315 --- /dev/null +++ b/netwerk/test/gtest/TestHttpResponseHead.cpp @@ -0,0 +1,113 @@ +#include "gtest/gtest.h" + +#include "chrome/common/ipc_message.h" +#include "mozilla/net/PHttpChannelParams.h" +#include "mozilla/Unused.h" +#include "nsHttp.h" + +namespace mozilla { +namespace net { + +void AssertRoundTrips(const nsHttpResponseHead& aHead) { + { + // Assert it round-trips via IPC. + UniquePtr<IPC::Message> msg(new IPC::Message(MSG_ROUTING_NONE, 0)); + IPC::MessageWriter writer(*msg); + IPC::ParamTraits<nsHttpResponseHead>::Write(&writer, aHead); + + nsHttpResponseHead deserializedHead; + IPC::MessageReader reader(*msg); + bool res = IPC::ParamTraits<mozilla::net::nsHttpResponseHead>::Read( + &reader, &deserializedHead); + ASSERT_TRUE(res); + ASSERT_EQ(aHead, deserializedHead); + } + + { + // Assert it round-trips through copy-ctor. + nsHttpResponseHead copied(aHead); + ASSERT_EQ(aHead, copied); + } + + { + // Assert it round-trips through operator= + nsHttpResponseHead copied; + copied = aHead; + ASSERT_EQ(aHead, copied); + } +} + +TEST(TestHttpResponseHead, Bug1636930) +{ + nsHttpResponseHead head; + + head.ParseStatusLine("HTTP/1.1 200 OK"_ns); + Unused << head.ParseHeaderLine("content-type: text/plain"_ns); + Unused << head.ParseHeaderLine("etag: Just testing"_ns); + Unused << head.ParseHeaderLine("cache-control: max-age=99999"_ns); + Unused << head.ParseHeaderLine("accept-ranges: bytes"_ns); + Unused << head.ParseHeaderLine("content-length: 1408"_ns); + Unused << head.ParseHeaderLine("connection: close"_ns); + Unused << head.ParseHeaderLine("server: httpd.js"_ns); + Unused << head.ParseHeaderLine("date: Tue, 12 May 2020 09:24:23 GMT"_ns); + + AssertRoundTrips(head); +} + +TEST(TestHttpResponseHead, bug1649807) +{ + nsHttpResponseHead head; + + head.ParseStatusLine("HTTP/1.1 200 OK"_ns); + Unused << head.ParseHeaderLine("content-type: text/plain"_ns); + Unused << head.ParseHeaderLine("etag: Just testing"_ns); + Unused << head.ParseHeaderLine("cache-control: age=99999"_ns); + Unused << head.ParseHeaderLine("accept-ranges: bytes"_ns); + Unused << head.ParseHeaderLine("content-length: 1408"_ns); + Unused << head.ParseHeaderLine("connection: close"_ns); + Unused << head.ParseHeaderLine("server: httpd.js"_ns); + Unused << head.ParseHeaderLine("pragma: no-cache"_ns); + Unused << head.ParseHeaderLine("date: Tue, 12 May 2020 09:24:23 GMT"_ns); + + ASSERT_FALSE(head.NoCache()) + << "Cache-Control wins over Pragma: no-cache"; + AssertRoundTrips(head); +} + +TEST(TestHttpResponseHead, bug1660200) +{ + nsHttpResponseHead head; + + head.ParseStatusLine("HTTP/1.1 200 OK"_ns); + Unused << head.ParseHeaderLine("content-type: text/plain"_ns); + Unused << head.ParseHeaderLine("etag: Just testing"_ns); + Unused << head.ParseHeaderLine("cache-control: no-cache"_ns); + Unused << head.ParseHeaderLine("accept-ranges: bytes"_ns); + Unused << head.ParseHeaderLine("content-length: 1408"_ns); + Unused << head.ParseHeaderLine("connection: close"_ns); + Unused << head.ParseHeaderLine("server: httpd.js"_ns); + Unused << head.ParseHeaderLine("date: Tue, 12 May 2020 09:24:23 GMT"_ns); + + AssertRoundTrips(head); +} + +TEST(TestHttpResponseHead, atoms) +{ + // Test that the resolving the content-type atom returns the initial static + ASSERT_EQ(nsHttp::Content_Type, nsHttp::ResolveAtom("content-type"_ns)); + // Check that they're case insensitive + ASSERT_EQ(nsHttp::ResolveAtom("Content-Type"_ns), + nsHttp::ResolveAtom("content-type"_ns)); + // This string literal should be the backing of the atom when resolved first + auto header1 = "CustomHeaderXXX1"_ns; + auto atom1 = nsHttp::ResolveAtom(header1); + auto header2 = "customheaderxxx1"_ns; + auto atom2 = nsHttp::ResolveAtom(header2); + ASSERT_EQ(atom1, atom2); + ASSERT_EQ(atom1.get(), atom2.get()); + // Check that we get the expected pointer back. + ASSERT_EQ(atom2.get(), header1.BeginReading()); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/test/gtest/TestInputStreamTransport.cpp b/netwerk/test/gtest/TestInputStreamTransport.cpp new file mode 100644 index 0000000000..43df0e193a --- /dev/null +++ b/netwerk/test/gtest/TestInputStreamTransport.cpp @@ -0,0 +1,204 @@ +#include "gtest/gtest.h" + +#include "nsIStreamTransportService.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "Helpers.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "nsITransport.h" +#include "nsNetUtil.h" + +static NS_DEFINE_CID(kStreamTransportServiceCID, NS_STREAMTRANSPORTSERVICE_CID); + +void CreateStream(already_AddRefed<nsIInputStream> aSource, + nsIAsyncInputStream** aStream) { + nsCOMPtr<nsIInputStream> source = std::move(aSource); + + nsresult rv; + nsCOMPtr<nsIStreamTransportService> sts = + do_GetService(kStreamTransportServiceCID, &rv); + ASSERT_EQ(NS_OK, rv); + + nsCOMPtr<nsITransport> transport; + rv = sts->CreateInputTransport(source, true, getter_AddRefs(transport)); + ASSERT_EQ(NS_OK, rv); + + nsCOMPtr<nsIInputStream> wrapper; + rv = transport->OpenInputStream(0, 0, 0, getter_AddRefs(wrapper)); + ASSERT_EQ(NS_OK, rv); + + nsCOMPtr<nsIAsyncInputStream> asyncStream = do_QueryInterface(wrapper); + MOZ_ASSERT(asyncStream); + + asyncStream.forget(aStream); +} + +class BlockingSyncStream final : public nsIInputStream { + nsCOMPtr<nsIInputStream> mStream; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit BlockingSyncStream(const nsACString& aBuffer) { + NS_NewCStringInputStream(getter_AddRefs(mStream), aBuffer); + } + + NS_IMETHOD + Available(uint64_t* aLength) override { return mStream->Available(aLength); } + + NS_IMETHOD + StreamStatus() override { return mStream->StreamStatus(); } + + NS_IMETHOD + Read(char* aBuffer, uint32_t aCount, uint32_t* aReadCount) override { + return mStream->Read(aBuffer, aCount, aReadCount); + } + + NS_IMETHOD + ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, uint32_t aCount, + uint32_t* aResult) override { + return mStream->ReadSegments(aWriter, aClosure, aCount, aResult); + } + + NS_IMETHOD + Close() override { return mStream->Close(); } + + NS_IMETHOD + IsNonBlocking(bool* aNonBlocking) override { + *aNonBlocking = false; + return NS_OK; + } + + private: + ~BlockingSyncStream() = default; +}; + +NS_IMPL_ISUPPORTS(BlockingSyncStream, nsIInputStream) + +// Testing a simple blocking stream. +TEST(TestInputStreamTransport, BlockingNotAsync) +{ + RefPtr<BlockingSyncStream> stream = new BlockingSyncStream("Hello world"_ns); + + nsCOMPtr<nsIAsyncInputStream> ais; + CreateStream(stream.forget(), getter_AddRefs(ais)); + ASSERT_TRUE(!!ais); + + nsAutoCString data; + nsresult rv = NS_ReadInputStreamToString(ais, data, -1); + ASSERT_EQ(NS_OK, rv); + + ASSERT_TRUE(data.EqualsLiteral("Hello world")); +} + +class BlockingAsyncStream final : public nsIAsyncInputStream { + nsCOMPtr<nsIInputStream> mStream; + bool mPending; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit BlockingAsyncStream(const nsACString& aBuffer) : mPending(false) { + NS_NewCStringInputStream(getter_AddRefs(mStream), aBuffer); + } + + NS_IMETHOD + Available(uint64_t* aLength) override { + mStream->Available(aLength); + + // 1 char at the time, just to test the asyncWait+Read loop a bit more. + if (*aLength > 0) { + *aLength = 1; + } + + return NS_OK; + } + + NS_IMETHOD + StreamStatus() override { return mStream->StreamStatus(); } + + NS_IMETHOD + Read(char* aBuffer, uint32_t aCount, uint32_t* aReadCount) override { + mPending = !mPending; + if (mPending) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + // 1 char at the time, just to test the asyncWait+Read loop a bit more. + aCount = 1; + + return mStream->Read(aBuffer, aCount, aReadCount); + } + + NS_IMETHOD + ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, uint32_t aCount, + uint32_t* aResult) override { + mPending = !mPending; + if (mPending) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + // 1 char at the time, just to test the asyncWait+Read loop a bit more. + aCount = 1; + + return mStream->ReadSegments(aWriter, aClosure, aCount, aResult); + } + + NS_IMETHOD + Close() override { return mStream->Close(); } + + NS_IMETHOD + IsNonBlocking(bool* aNonBlocking) override { + *aNonBlocking = false; + return NS_OK; + } + + NS_IMETHOD + CloseWithStatus(nsresult aStatus) override { return Close(); } + + NS_IMETHOD + AsyncWait(nsIInputStreamCallback* aCallback, uint32_t aFlags, + uint32_t aRequestedCount, nsIEventTarget* aEventTarget) override { + if (!aCallback) { + return NS_OK; + } + + RefPtr<BlockingAsyncStream> self = this; + nsCOMPtr<nsIInputStreamCallback> callback = aCallback; + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "gtest-asyncwait", + [self, callback]() { callback->OnInputStreamReady(self); }); + + if (aEventTarget) { + aEventTarget->Dispatch(r.forget()); + } else { + r->Run(); + } + + return NS_OK; + } + + private: + ~BlockingAsyncStream() = default; +}; + +NS_IMPL_ISUPPORTS(BlockingAsyncStream, nsIInputStream, nsIAsyncInputStream) + +// Testing an async blocking stream. +TEST(TestInputStreamTransport, BlockingAsync) +{ + RefPtr<BlockingAsyncStream> stream = + new BlockingAsyncStream("Hello world"_ns); + + nsCOMPtr<nsIAsyncInputStream> ais; + CreateStream(stream.forget(), getter_AddRefs(ais)); + ASSERT_TRUE(!!ais); + + nsAutoCString data; + nsresult rv = NS_ReadInputStreamToString(ais, data, -1); + ASSERT_EQ(NS_OK, rv); + + ASSERT_TRUE(data.EqualsLiteral("Hello world")); +} diff --git a/netwerk/test/gtest/TestIsValidIp.cpp b/netwerk/test/gtest/TestIsValidIp.cpp new file mode 100644 index 0000000000..dfee8c5c0f --- /dev/null +++ b/netwerk/test/gtest/TestIsValidIp.cpp @@ -0,0 +1,178 @@ +#include "gtest/MozGTestBench.h" // For MOZ_GTEST_BENCH +#include "gtest/gtest.h" + +#include "nsURLHelper.h" + +TEST(TestIsValidIp, IPV4Localhost) +{ + constexpr auto ip = "127.0.0.1"_ns; + ASSERT_EQ(true, net_IsValidIPv4Addr(ip)); +} + +TEST(TestIsValidIp, IPV4Only0) +{ + constexpr auto ip = "0.0.0.0"_ns; + ASSERT_EQ(true, net_IsValidIPv4Addr(ip)); +} + +TEST(TestIsValidIp, IPV4Max) +{ + constexpr auto ip = "255.255.255.255"_ns; + ASSERT_EQ(true, net_IsValidIPv4Addr(ip)); +} + +TEST(TestIsValidIp, IPV4LeadingZero) +{ + constexpr auto ip = "055.225.255.255"_ns; + ASSERT_EQ(false, net_IsValidIPv4Addr(ip)); + + constexpr auto ip2 = "255.055.255.255"_ns; + ASSERT_EQ(false, net_IsValidIPv4Addr(ip2)); + + constexpr auto ip3 = "255.255.055.255"_ns; + ASSERT_EQ(false, net_IsValidIPv4Addr(ip3)); + + constexpr auto ip4 = "255.255.255.055"_ns; + ASSERT_EQ(false, net_IsValidIPv4Addr(ip4)); +} + +TEST(TestIsValidIp, IPV4StartWithADot) +{ + constexpr auto ip = ".192.168.120.197"_ns; + ASSERT_EQ(false, net_IsValidIPv4Addr(ip)); +} + +TEST(TestIsValidIp, IPV4StartWith4Digits) +{ + constexpr auto ip = "1927.168.120.197"_ns; + ASSERT_EQ(false, net_IsValidIPv4Addr(ip)); +} + +TEST(TestIsValidIp, IPV4OutOfRange) +{ + constexpr auto invalid1 = "421.168.120.124"_ns; + constexpr auto invalid2 = "192.997.120.124"_ns; + constexpr auto invalid3 = "192.168.300.124"_ns; + constexpr auto invalid4 = "192.168.120.256"_ns; + + ASSERT_EQ(false, net_IsValidIPv4Addr(invalid1)); + ASSERT_EQ(false, net_IsValidIPv4Addr(invalid2)); + ASSERT_EQ(false, net_IsValidIPv4Addr(invalid3)); + ASSERT_EQ(false, net_IsValidIPv4Addr(invalid4)); +} + +TEST(TestIsValidIp, IPV4EmptyDigits) +{ + constexpr auto invalid1 = "..0.0.0"_ns; + constexpr auto invalid2 = "127..0.0"_ns; + constexpr auto invalid3 = "127.0..0"_ns; + constexpr auto invalid4 = "127.0.0."_ns; + + ASSERT_EQ(false, net_IsValidIPv4Addr(invalid1)); + ASSERT_EQ(false, net_IsValidIPv4Addr(invalid2)); + ASSERT_EQ(false, net_IsValidIPv4Addr(invalid3)); + ASSERT_EQ(false, net_IsValidIPv4Addr(invalid4)); +} + +TEST(TestIsValidIp, IPV4NonNumeric) +{ + constexpr auto invalid1 = "127.0.0.f"_ns; + constexpr auto invalid2 = "127.0.0.!"_ns; + constexpr auto invalid3 = "127#0.0.1"_ns; + + ASSERT_EQ(false, net_IsValidIPv4Addr(invalid1)); + ASSERT_EQ(false, net_IsValidIPv4Addr(invalid2)); + ASSERT_EQ(false, net_IsValidIPv4Addr(invalid3)); +} + +TEST(TestIsValidIp, IPV4TooManyDigits) +{ + constexpr auto ip = "127.0.0.1.2"_ns; + ASSERT_EQ(false, net_IsValidIPv4Addr(ip)); +} + +TEST(TestIsValidIp, IPV4TooFewDigits) +{ + constexpr auto ip = "127.0.1"_ns; + ASSERT_EQ(false, net_IsValidIPv4Addr(ip)); +} + +TEST(TestIsValidIp, IPV6WithIPV4Inside) +{ + constexpr auto ipv6 = "0123:4567:89ab:cdef:0123:4567:127.0.0.1"_ns; + ASSERT_EQ(true, net_IsValidIPv6Addr(ipv6)); +} + +TEST(TestIsValidIp, IPv6FullForm) +{ + constexpr auto ipv6 = "0123:4567:89ab:cdef:0123:4567:890a:bcde"_ns; + ASSERT_EQ(true, net_IsValidIPv6Addr(ipv6)); +} + +TEST(TestIsValidIp, IPv6TrimLeading0) +{ + constexpr auto ipv6 = "123:4567:0:0:123:4567:890a:bcde"_ns; + ASSERT_EQ(true, net_IsValidIPv6Addr(ipv6)); +} + +TEST(TestIsValidIp, IPv6Collapsed) +{ + constexpr auto ipv6 = "FF01::101"_ns; + ASSERT_EQ(true, net_IsValidIPv6Addr(ipv6)); +} + +TEST(TestIsValidIp, IPV6WithIPV4InsideCollapsed) +{ + constexpr auto ipv6 = "::FFFF:129.144.52.38"_ns; + ASSERT_EQ(true, net_IsValidIPv6Addr(ipv6)); +} + +TEST(TestIsValidIp, IPV6Localhost) +{ + constexpr auto ipv6 = "::1"_ns; + ASSERT_EQ(true, net_IsValidIPv6Addr(ipv6)); +} + +TEST(TestIsValidIp, IPV6LinkLocalPrefix) +{ + constexpr auto ipv6 = "fe80::"_ns; + ASSERT_EQ(true, net_IsValidIPv6Addr(ipv6)); +} + +TEST(TestIsValidIp, IPV6GlobalUnicastPrefix) +{ + constexpr auto ipv6 = "2001::"_ns; + ASSERT_EQ(true, net_IsValidIPv6Addr(ipv6)); +} + +TEST(TestIsValidIp, IPV6Unspecified) +{ + constexpr auto ipv6 = "::"_ns; + ASSERT_EQ(true, net_IsValidIPv6Addr(ipv6)); +} + +TEST(TestIsValidIp, IPV6InvalidIPV4Inside) +{ + constexpr auto ipv6 = "0123:4567:89ab:cdef:0123:4567:127.0."_ns; + ASSERT_EQ(false, net_IsValidIPv6Addr(ipv6)); +} + +TEST(TestIsValidIp, IPV6InvalidCharacters) +{ + constexpr auto ipv6 = "012g:4567:89ab:cdef:0123:4567:127.0.0.1"_ns; + ASSERT_EQ(false, net_IsValidIPv6Addr(ipv6)); + + constexpr auto ipv6pound = "0123:456#:89ab:cdef:0123:4567:127.0.0.1"_ns; + ASSERT_EQ(false, net_IsValidIPv6Addr(ipv6pound)); +} + +TEST(TestIsValidIp, IPV6TooManyCharacters) +{ + constexpr auto ipv6 = "0123:45671:89ab:cdef:0123:4567:127.0.0.1"_ns; + ASSERT_EQ(false, net_IsValidIPv6Addr(ipv6)); +} +TEST(TestIsValidIp, IPV6DoubleDoubleDots) +{ + constexpr auto ipv6 = "0123::4567:890a::bcde:0123:4567"_ns; + ASSERT_EQ(false, net_IsValidIPv6Addr(ipv6)); +} diff --git a/netwerk/test/gtest/TestLinkHeader.cpp b/netwerk/test/gtest/TestLinkHeader.cpp new file mode 100644 index 0000000000..fe3e1baf86 --- /dev/null +++ b/netwerk/test/gtest/TestLinkHeader.cpp @@ -0,0 +1,307 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest-param-test.h" +#include "gtest/gtest.h" + +#include "mozilla/gtest/MozAssertions.h" +#include "nsNetUtil.h" + +using namespace mozilla::net; + +LinkHeader LinkHeaderSetAll(nsAString const& v) { + LinkHeader l; + l.mHref = v; + l.mRel = v; + l.mTitle = v; + l.mIntegrity = v; + l.mSrcset = v; + l.mSizes = v; + l.mType = v; + l.mMedia = v; + l.mAnchor = v; + l.mCrossOrigin = v; + l.mReferrerPolicy = v; + l.mAs = v; + return l; +} + +LinkHeader LinkHeaderSetTitle(nsAString const& v) { + LinkHeader l; + l.mHref = v; + l.mRel = v; + l.mTitle = v; + return l; +} + +LinkHeader LinkHeaderSetMinimum(nsAString const& v) { + LinkHeader l; + l.mHref = v; + l.mRel = v; + return l; +} + +TEST(TestLinkHeader, MultipleLinkHeaders) +{ + nsString link = + u"<a>; rel=a; title=a; integrity=a; imagesrcset=a; imagesizes=a; type=a; media=a; anchor=a; crossorigin=a; referrerpolicy=a; as=a,"_ns + u"<b>; rel=b; title=b; integrity=b; imagesrcset=b; imagesizes=b; type=b; media=b; anchor=b; crossorigin=b; referrerpolicy=b; as=b,"_ns + u"<c>; rel=c"_ns; + + nsTArray<LinkHeader> linkHeaders = ParseLinkHeader(link); + + nsTArray<LinkHeader> expected; + expected.AppendElement(LinkHeaderSetAll(u"a"_ns)); + expected.AppendElement(LinkHeaderSetAll(u"b"_ns)); + expected.AppendElement(LinkHeaderSetMinimum(u"c"_ns)); + + ASSERT_EQ(linkHeaders, expected); +} + +// title* has to be tested separately +TEST(TestLinkHeader, MultipleLinkHeadersTitleStar) +{ + nsString link = + u"<d>; rel=d; title*=UTF-8'de'd,"_ns + u"<e>; rel=e; title*=UTF-8'de'e; title=g,"_ns + u"<f>; rel=f"_ns; + + nsTArray<LinkHeader> linkHeaders = ParseLinkHeader(link); + + nsTArray<LinkHeader> expected; + expected.AppendElement(LinkHeaderSetTitle(u"d"_ns)); + expected.AppendElement(LinkHeaderSetTitle(u"e"_ns)); + expected.AppendElement(LinkHeaderSetMinimum(u"f"_ns)); + + ASSERT_EQ(linkHeaders, expected); +} + +struct SimpleParseTestData { + nsString link; + bool valid; + nsString url; + nsString rel; + nsString as; +}; + +class SimpleParseTest : public ::testing::TestWithParam<SimpleParseTestData> {}; + +TEST_P(SimpleParseTest, Simple) { + const SimpleParseTestData test = GetParam(); + + nsTArray<LinkHeader> linkHeaders = ParseLinkHeader(test.link); + + EXPECT_EQ(test.valid, !linkHeaders.IsEmpty()); + if (test.valid) { + ASSERT_EQ(linkHeaders.Length(), (nsTArray<LinkHeader>::size_type)1); + EXPECT_EQ(test.url, linkHeaders[0].mHref); + EXPECT_EQ(test.rel, linkHeaders[0].mRel); + EXPECT_EQ(test.as, linkHeaders[0].mAs); + } +} + +// Test copied and adapted from +// https://source.chromium.org/chromium/chromium/src/+/main:components/link_header_util/link_header_util_unittest.cc +// the different behavior of the parser is commented above each test case. +const SimpleParseTestData simple_parse_tests[] = { + {u"</images/cat.jpg>; rel=prefetch"_ns, true, u"/images/cat.jpg"_ns, + u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg>;rel=prefetch"_ns, true, u"/images/cat.jpg"_ns, + u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg> ;rel=prefetch"_ns, true, u"/images/cat.jpg"_ns, + u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg> ; rel=prefetch"_ns, true, u"/images/cat.jpg"_ns, + u"prefetch"_ns, u""_ns}, + {u"< /images/cat.jpg> ; rel=prefetch"_ns, true, u"/images/cat.jpg"_ns, + u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg > ; rel=prefetch"_ns, true, u"/images/cat.jpg"_ns, + u"prefetch"_ns, u""_ns}, + // TODO(1744051): don't ignore spaces in href + // {u"</images/cat.jpg wutwut> ; rel=prefetch"_ns, true, + // u"/images/cat.jpg wutwut"_ns, u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg wutwut> ; rel=prefetch"_ns, true, + u"/images/cat.jpgwutwut"_ns, u"prefetch"_ns, u""_ns}, + // TODO(1744051): don't ignore spaces in href + // {u"</images/cat.jpg wutwut \t > ; rel=prefetch"_ns, true, + // u"/images/cat.jpg wutwut"_ns, u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg wutwut \t > ; rel=prefetch"_ns, true, + u"/images/cat.jpgwutwut"_ns, u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg>; rel=prefetch "_ns, true, u"/images/cat.jpg"_ns, + u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg>; Rel=prefetch "_ns, true, u"/images/cat.jpg"_ns, + u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg>; Rel=PReFetCh "_ns, true, u"/images/cat.jpg"_ns, + u"PReFetCh"_ns, u""_ns}, + {u"</images/cat.jpg>; rel=prefetch; rel=somethingelse"_ns, true, + u"/images/cat.jpg"_ns, u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg>\t\t ; \trel=prefetch \t "_ns, true, + u"/images/cat.jpg"_ns, u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg>; rel= prefetch"_ns, true, u"/images/cat.jpg"_ns, + u"prefetch"_ns, u""_ns}, + {u"<../images/cat.jpg?dog>; rel= prefetch"_ns, true, + u"../images/cat.jpg?dog"_ns, u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg>; rel =prefetch"_ns, true, u"/images/cat.jpg"_ns, + u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg>; rel pel=prefetch"_ns, false}, + // different from chromium test case, because we already check for + // existence of "rel" parameter + {u"< /images/cat.jpg>"_ns, false}, + {u"</images/cat.jpg>; wut=sup; rel =prefetch"_ns, true, + u"/images/cat.jpg"_ns, u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg>; wut=sup ; rel =prefetch"_ns, true, + u"/images/cat.jpg"_ns, u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg>; wut=sup ; rel =prefetch \t ;"_ns, true, + u"/images/cat.jpg"_ns, u"prefetch"_ns, u""_ns}, + // TODO(1744051): forbid non-whitespace characters between '>' and the first + // semicolon making it conform RFC 8288 Sec 3 + // {u"</images/cat.jpg> wut=sup ; rel =prefetch \t ;"_ns, false}, + {u"</images/cat.jpg> wut=sup ; rel =prefetch \t ;"_ns, true, + u"/images/cat.jpg"_ns, u"prefetch"_ns, u""_ns}, + {u"< /images/cat.jpg"_ns, false}, + // TODO(1744051): don't ignore spaces in href + // {u"< http://wut.com/ sdfsdf ?sd>; rel=dns-prefetch"_ns, true, + // u"http://wut.com/ sdfsdf ?sd"_ns, u"dns-prefetch"_ns, u""_ns}, + {u"< http://wut.com/ sdfsdf ?sd>; rel=dns-prefetch"_ns, true, + u"http://wut.com/sdfsdf?sd"_ns, u"dns-prefetch"_ns, u""_ns}, + {u"< http://wut.com/%20%20%3dsdfsdf?sd>; rel=dns-prefetch"_ns, true, + u"http://wut.com/%20%20%3dsdfsdf?sd"_ns, u"dns-prefetch"_ns, u""_ns}, + {u"< http://wut.com/dfsdf?sdf=ghj&wer=rty>; rel=prefetch"_ns, true, + u"http://wut.com/dfsdf?sdf=ghj&wer=rty"_ns, u"prefetch"_ns, u""_ns}, + {u"< http://wut.com/dfsdf?sdf=ghj&wer=rty>;;;;; rel=prefetch"_ns, true, + u"http://wut.com/dfsdf?sdf=ghj&wer=rty"_ns, u"prefetch"_ns, u""_ns}, + {u"< http://wut.com/%20%20%3dsdfsdf?sd>; rel=preload;as=image"_ns, true, + u"http://wut.com/%20%20%3dsdfsdf?sd"_ns, u"preload"_ns, u"image"_ns}, + {u"< http://wut.com/%20%20%3dsdfsdf?sd>; rel=preload;as=whatever"_ns, + true, u"http://wut.com/%20%20%3dsdfsdf?sd"_ns, u"preload"_ns, + u"whatever"_ns}, + {u"</images/cat.jpg>; rel=prefetch;"_ns, true, u"/images/cat.jpg"_ns, + u"prefetch"_ns, u""_ns}, + {u"</images/cat.jpg>; rel=prefetch ;"_ns, true, u"/images/cat.jpg"_ns, + u"prefetch"_ns, u""_ns}, + {u"</images/ca,t.jpg>; rel=prefetch ;"_ns, true, u"/images/ca,t.jpg"_ns, + u"prefetch"_ns, u""_ns}, + {u"<simple.css>; rel=stylesheet; title=\"title with a DQUOTE and " + "backslash\""_ns, + true, u"simple.css"_ns, u"stylesheet"_ns, u""_ns}, + // TODO(1744051): forbid missing end quote + // {u"<simple.css>; rel=stylesheet; title=\"title with a DQUOTE \\\" and " + // "backslash: \\\""_ns, false}, + {u"<simple.css>; rel=stylesheet; title=\"title with a DQUOTE \\\" and backslash: \\\""_ns, + true, u"simple.css"_ns, u"stylesheet"_ns, u""_ns}, + {u"<simple.css>; title=\"title with a DQUOTE \\\" and backslash: \"; " + "rel=stylesheet; "_ns, + true, u"simple.css"_ns, u"stylesheet"_ns, u""_ns}, + {u"<simple.css>; title=\'title with a DQUOTE \\\' and backslash: \'; " + "rel=stylesheet; "_ns, + true, u"simple.css"_ns, u"stylesheet"_ns, u""_ns}, + {u"<simple.css>; title=\"title with a DQUOTE \\\" and ;backslash,: \"; " + "rel=stylesheet; "_ns, + true, u"simple.css"_ns, u"stylesheet"_ns, u""_ns}, + {u"<simple.css>; title=\"title with a DQUOTE \' and ;backslash,: \"; " + "rel=stylesheet; "_ns, + true, u"simple.css"_ns, u"stylesheet"_ns, u""_ns}, + {u"<simple.css>; title=\"\"; rel=stylesheet; "_ns, true, u"simple.css"_ns, + u"stylesheet"_ns, u""_ns}, + {u"<simple.css>; title=\"\"; rel=\"stylesheet\"; "_ns, true, + u"simple.css"_ns, u"stylesheet"_ns, u""_ns}, + // TODO(1744051): forbid missing end quote + // {u"<simple.css>; rel=stylesheet; title=\""_ns, false}, + {u"<simple.css>; rel=stylesheet; title=\""_ns, true, u"simple.css"_ns, + u"stylesheet"_ns, u""_ns}, + {u"<simple.css>; rel=stylesheet; title=\"\""_ns, true, u"simple.css"_ns, + u"stylesheet"_ns, u""_ns}, + // TODO(1744051): forbid missing end quote + // {u"<simple.css>; rel=\"stylesheet\"; title=\""_ns, false}, + {u"<simple.css>; rel=\"stylesheet\"; title=\""_ns, true, u"simple.css"_ns, + u"stylesheet"_ns, u""_ns}, + // TODO(1744051): forbid missing end quote + // {u"<simple.css>; rel=\";style,sheet\"; title=\""_ns, false}, + {u"<simple.css>; rel=\";style,sheet\"; title=\""_ns, true, u"simple.css"_ns, + u";style,sheet"_ns, u""_ns}, + // TODO(1744051): forbid missing end quote + // {u"<simple.css>; rel=\"bla'sdf\"; title=\""_ns, false} + {u"<simple.css>; rel=\"bla'sdf\"; title=\""_ns, true, u"simple.css"_ns, + u"bla'sdf"_ns, u""_ns}, + // TODO(1744051): allow explicit empty rel + // {u"<simple.css>; rel=\"\"; title=\"\""_ns, true, u"simple.css"_ns, + // u""_ns, u""_ns} + {u"<simple.css>; rel=\"\"; title=\"\""_ns, false}, + {u"<simple.css>; rel=''; title=\"\""_ns, true, u"simple.css"_ns, u"''"_ns, + u""_ns}, + {u"<simple.css>; rel=''; bla"_ns, true, u"simple.css"_ns, u"''"_ns, u""_ns}, + {u"<simple.css>; rel='prefetch"_ns, true, u"simple.css"_ns, u"'prefetch"_ns, + u""_ns}, + // TODO(1744051): forbid missing end quote + // {u"<simple.css>; rel=\"prefetch"_ns, false}, + {u"<simple.css>; rel=\"prefetch"_ns, true, u"simple.css"_ns, + u"\"prefetch"_ns, u""_ns}, + {u"<simple.css>; rel=\""_ns, false}, + {u"simple.css; rel=prefetch"_ns, false}, + {u"<simple.css>; rel=prefetch; rel=foobar"_ns, true, u"simple.css"_ns, + u"prefetch"_ns, u""_ns}, +}; + +INSTANTIATE_TEST_SUITE_P(TestLinkHeader, SimpleParseTest, + testing::ValuesIn(simple_parse_tests)); + +// Test anchor + +struct AnchorTestData { + nsString baseURI; + // building the new anchor in combination with the baseURI + nsString anchor; + nsString href; + const char* resolved; +}; + +class AnchorTest : public ::testing::TestWithParam<AnchorTestData> {}; + +const AnchorTestData anchor_tests[] = { + {u"http://example.com/path/to/index.html"_ns, u""_ns, u"page.html"_ns, + "http://example.com/path/to/page.html"}, + {u"http://example.com/path/to/index.html"_ns, + u"http://example.com/path/"_ns, u"page.html"_ns, + "http://example.com/path/page.html"}, + {u"http://example.com/path/to/index.html"_ns, + u"http://example.com/path/"_ns, u"/page.html"_ns, + "http://example.com/page.html"}, + {u"http://example.com/path/to/index.html"_ns, u".."_ns, u"page.html"_ns, + "http://example.com/path/page.html"}, + {u"http://example.com/path/to/index.html"_ns, u".."_ns, + u"from/page.html"_ns, "http://example.com/path/from/page.html"}, + {u"http://example.com/path/to/index.html"_ns, u"/hello/"_ns, + u"page.html"_ns, "http://example.com/hello/page.html"}, + {u"http://example.com/path/to/index.html"_ns, u"/hello"_ns, u"page.html"_ns, + "http://example.com/page.html"}, + {u"http://example.com/path/to/index.html"_ns, u"#necko"_ns, u"page.html"_ns, + "http://example.com/path/to/page.html"}, + {u"http://example.com/path/to/index.html"_ns, u"https://example.net/"_ns, + u"to/page.html"_ns, "https://example.net/to/page.html"}, +}; + +LinkHeader LinkHeaderFromHrefAndAnchor(nsAString const& aHref, + nsAString const& aAnchor) { + LinkHeader l; + l.mHref = aHref; + l.mAnchor = aAnchor; + return l; +} + +TEST_P(AnchorTest, Anchor) { + const AnchorTestData test = GetParam(); + + LinkHeader linkHeader = LinkHeaderFromHrefAndAnchor(test.href, test.anchor); + + nsCOMPtr<nsIURI> baseURI; + ASSERT_NS_SUCCEEDED(NS_NewURI(getter_AddRefs(baseURI), test.baseURI)); + + nsCOMPtr<nsIURI> resolved; + ASSERT_TRUE(NS_SUCCEEDED( + linkHeader.NewResolveHref(getter_AddRefs(resolved), baseURI))); + + ASSERT_STREQ(resolved->GetSpecOrDefault().get(), test.resolved); +} + +INSTANTIATE_TEST_SUITE_P(TestLinkHeader, AnchorTest, + testing::ValuesIn(anchor_tests)); diff --git a/netwerk/test/gtest/TestMIMEInputStream.cpp b/netwerk/test/gtest/TestMIMEInputStream.cpp new file mode 100644 index 0000000000..a2f3f7a43d --- /dev/null +++ b/netwerk/test/gtest/TestMIMEInputStream.cpp @@ -0,0 +1,268 @@ +#include "gtest/gtest.h" + +#include "Helpers.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "nsStringStream.h" +#include "nsIMIMEInputStream.h" +#include "nsISeekableStream.h" + +using mozilla::GetCurrentSerialEventTarget; +using mozilla::SpinEventLoopUntil; + +namespace { + +class SeekableLengthInputStream final : public testing::LengthInputStream, + public nsISeekableStream { + public: + SeekableLengthInputStream(const nsACString& aBuffer, + bool aIsInputStreamLength, + bool aIsAsyncInputStreamLength, + nsresult aLengthRv = NS_OK, + bool aNegativeValue = false) + : testing::LengthInputStream(aBuffer, aIsInputStreamLength, + aIsAsyncInputStreamLength, aLengthRv, + aNegativeValue) {} + + NS_DECL_ISUPPORTS_INHERITED + + NS_IMETHOD + Seek(int32_t aWhence, int64_t aOffset) override { + MOZ_CRASH("This method should not be called."); + return NS_ERROR_FAILURE; + } + + NS_IMETHOD + Tell(int64_t* aResult) override { + MOZ_CRASH("This method should not be called."); + return NS_ERROR_FAILURE; + } + + NS_IMETHOD + SetEOF() override { + MOZ_CRASH("This method should not be called."); + return NS_ERROR_FAILURE; + } + + private: + ~SeekableLengthInputStream() = default; +}; + +NS_IMPL_ISUPPORTS_INHERITED(SeekableLengthInputStream, + testing::LengthInputStream, nsISeekableStream) + +} // namespace + +// nsIInputStreamLength && nsIAsyncInputStreamLength + +TEST(TestNsMIMEInputStream, QIInputStreamLength) +{ + nsCString buf; + buf.AssignLiteral("Hello world"); + + for (int i = 0; i < 4; i++) { + nsCOMPtr<nsIInputStream> mis; + { + RefPtr<SeekableLengthInputStream> stream = + new SeekableLengthInputStream(buf, i % 2, i > 1); + + nsresult rv; + nsCOMPtr<nsIMIMEInputStream> m( + do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv)); + ASSERT_EQ(NS_OK, rv); + + rv = m->SetData(stream); + ASSERT_EQ(NS_OK, rv); + + mis = m; + ASSERT_TRUE(!!mis); + } + + { + nsCOMPtr<nsIInputStreamLength> qi = do_QueryInterface(mis); + ASSERT_EQ(!!(i % 2), !!qi); + } + + { + nsCOMPtr<nsIAsyncInputStreamLength> qi = do_QueryInterface(mis); + ASSERT_EQ(i > 1, !!qi); + } + } +} + +TEST(TestNsMIMEInputStream, InputStreamLength) +{ + nsCString buf; + buf.AssignLiteral("Hello world"); + + nsCOMPtr<nsIInputStream> mis; + { + RefPtr<SeekableLengthInputStream> stream = + new SeekableLengthInputStream(buf, true, false); + + nsresult rv; + nsCOMPtr<nsIMIMEInputStream> m( + do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv)); + ASSERT_EQ(NS_OK, rv); + + rv = m->SetData(stream); + ASSERT_EQ(NS_OK, rv); + + mis = m; + ASSERT_TRUE(!!mis); + } + + nsCOMPtr<nsIInputStreamLength> qi = do_QueryInterface(mis); + ASSERT_TRUE(!!qi); + + int64_t size; + nsresult rv = qi->Length(&size); + ASSERT_EQ(NS_OK, rv); + ASSERT_EQ(int64_t(buf.Length()), size); +} + +TEST(TestNsMIMEInputStream, NegativeInputStreamLength) +{ + nsCString buf; + buf.AssignLiteral("Hello world"); + + nsCOMPtr<nsIInputStream> mis; + { + RefPtr<SeekableLengthInputStream> stream = + new SeekableLengthInputStream(buf, true, false, NS_OK, true); + + nsresult rv; + nsCOMPtr<nsIMIMEInputStream> m( + do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv)); + ASSERT_EQ(NS_OK, rv); + + rv = m->SetData(stream); + ASSERT_EQ(NS_OK, rv); + + mis = m; + ASSERT_TRUE(!!mis); + } + + nsCOMPtr<nsIInputStreamLength> qi = do_QueryInterface(mis); + ASSERT_TRUE(!!qi); + + int64_t size; + nsresult rv = qi->Length(&size); + ASSERT_EQ(NS_OK, rv); + ASSERT_EQ(-1, size); +} + +TEST(TestNsMIMEInputStream, AsyncInputStreamLength) +{ + nsCString buf; + buf.AssignLiteral("Hello world"); + + nsCOMPtr<nsIInputStream> mis; + { + RefPtr<SeekableLengthInputStream> stream = + new SeekableLengthInputStream(buf, false, true); + + nsresult rv; + nsCOMPtr<nsIMIMEInputStream> m( + do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv)); + ASSERT_EQ(NS_OK, rv); + + rv = m->SetData(stream); + ASSERT_EQ(NS_OK, rv); + + mis = m; + ASSERT_TRUE(!!mis); + } + + nsCOMPtr<nsIAsyncInputStreamLength> qi = do_QueryInterface(mis); + ASSERT_TRUE(!!qi); + + RefPtr<testing::LengthCallback> callback = new testing::LengthCallback(); + + nsresult rv = qi->AsyncLengthWait(callback, GetCurrentSerialEventTarget()); + ASSERT_EQ(NS_OK, rv); + + MOZ_ALWAYS_TRUE(SpinEventLoopUntil( + "TEST(TestNsMIMEInputStream, AsyncInputStreamLength)"_ns, + [&]() { return callback->Called(); })); + ASSERT_EQ(int64_t(buf.Length()), callback->Size()); +} + +TEST(TestNsMIMEInputStream, NegativeAsyncInputStreamLength) +{ + nsCString buf; + buf.AssignLiteral("Hello world"); + + nsCOMPtr<nsIInputStream> mis; + { + RefPtr<SeekableLengthInputStream> stream = + new SeekableLengthInputStream(buf, false, true, NS_OK, true); + + nsresult rv; + nsCOMPtr<nsIMIMEInputStream> m( + do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv)); + ASSERT_EQ(NS_OK, rv); + + rv = m->SetData(stream); + ASSERT_EQ(NS_OK, rv); + + mis = m; + ASSERT_TRUE(!!mis); + } + + nsCOMPtr<nsIAsyncInputStreamLength> qi = do_QueryInterface(mis); + ASSERT_TRUE(!!qi); + + RefPtr<testing::LengthCallback> callback = new testing::LengthCallback(); + + nsresult rv = qi->AsyncLengthWait(callback, GetCurrentSerialEventTarget()); + ASSERT_EQ(NS_OK, rv); + + MOZ_ALWAYS_TRUE(SpinEventLoopUntil( + "TEST(TestNsMIMEInputStream, NegativeAsyncInputStreamLength)"_ns, + [&]() { return callback->Called(); })); + ASSERT_EQ(-1, callback->Size()); +} + +TEST(TestNsMIMEInputStream, AbortLengthCallback) +{ + nsCString buf; + buf.AssignLiteral("Hello world"); + + nsCOMPtr<nsIInputStream> mis; + { + RefPtr<SeekableLengthInputStream> stream = + new SeekableLengthInputStream(buf, false, true, NS_OK, true); + + nsresult rv; + nsCOMPtr<nsIMIMEInputStream> m( + do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv)); + ASSERT_EQ(NS_OK, rv); + + rv = m->SetData(stream); + ASSERT_EQ(NS_OK, rv); + + mis = m; + ASSERT_TRUE(!!mis); + } + + nsCOMPtr<nsIAsyncInputStreamLength> qi = do_QueryInterface(mis); + ASSERT_TRUE(!!qi); + + RefPtr<testing::LengthCallback> callback1 = new testing::LengthCallback(); + nsresult rv = qi->AsyncLengthWait(callback1, GetCurrentSerialEventTarget()); + ASSERT_EQ(NS_OK, rv); + + RefPtr<testing::LengthCallback> callback2 = new testing::LengthCallback(); + rv = qi->AsyncLengthWait(callback2, GetCurrentSerialEventTarget()); + ASSERT_EQ(NS_OK, rv); + + MOZ_ALWAYS_TRUE( + SpinEventLoopUntil("TEST(TestNsMIMEInputStream, AbortLengthCallback)"_ns, + [&]() { return callback2->Called(); })); + ASSERT_TRUE(!callback1->Called()); + ASSERT_EQ(-1, callback2->Size()); +} diff --git a/netwerk/test/gtest/TestMozURL.cpp b/netwerk/test/gtest/TestMozURL.cpp new file mode 100644 index 0000000000..76ec6db384 --- /dev/null +++ b/netwerk/test/gtest/TestMozURL.cpp @@ -0,0 +1,390 @@ +#include "gtest/gtest.h" +#include "gtest/MozGTestBench.h" // For MOZ_GTEST_BENCH + +#include <regex> +#include "json/json.h" +#include "json/reader.h" +#include "mozilla/TextUtils.h" +#include "nsString.h" +#include "mozilla/net/MozURL.h" +#include "nsCOMPtr.h" +#include "nsDirectoryServiceDefs.h" +#include "nsNetUtil.h" +#include "nsIFile.h" +#include "nsIURI.h" +#include "nsStreamUtils.h" +#include "mozilla/BasePrincipal.h" + +using namespace mozilla; +using namespace mozilla::net; + +TEST(TestMozURL, Getters) +{ + nsAutoCString href("http://user:pass@example.com/path?query#ref"); + RefPtr<MozURL> url; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK); + + ASSERT_TRUE(url->Scheme().EqualsLiteral("http")); + + ASSERT_TRUE(url->Spec() == href); + + ASSERT_TRUE(url->Username().EqualsLiteral("user")); + + ASSERT_TRUE(url->Password().EqualsLiteral("pass")); + + ASSERT_TRUE(url->Host().EqualsLiteral("example.com")); + + ASSERT_TRUE(url->FilePath().EqualsLiteral("/path")); + + ASSERT_TRUE(url->Query().EqualsLiteral("query")); + + ASSERT_TRUE(url->Ref().EqualsLiteral("ref")); + + url = nullptr; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url), ""_ns), NS_ERROR_MALFORMED_URI); + ASSERT_EQ(url, nullptr); +} + +TEST(TestMozURL, MutatorChain) +{ + nsAutoCString href("http://user:pass@example.com/path?query#ref"); + RefPtr<MozURL> url; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK); + nsAutoCString out; + + RefPtr<MozURL> url2; + ASSERT_EQ(url->Mutate() + .SetScheme("https"_ns) + .SetUsername("newuser"_ns) + .SetPassword("newpass"_ns) + .SetHostname("test"_ns) + .SetFilePath("new/file/path"_ns) + .SetQuery("bla"_ns) + .SetRef("huh"_ns) + .Finalize(getter_AddRefs(url2)), + NS_OK); + + ASSERT_TRUE(url2->Spec().EqualsLiteral( + "https://newuser:newpass@test/new/file/path?bla#huh")); +} + +TEST(TestMozURL, MutatorFinalizeTwice) +{ + nsAutoCString href("http://user:pass@example.com/path?query#ref"); + RefPtr<MozURL> url; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK); + nsAutoCString out; + + RefPtr<MozURL> url2; + MozURL::Mutator mut = url->Mutate(); + mut.SetScheme("https"_ns); // Change the scheme to https + ASSERT_EQ(mut.Finalize(getter_AddRefs(url2)), NS_OK); + ASSERT_TRUE(url2->Spec().EqualsLiteral( + "https://user:pass@example.com/path?query#ref")); + + // Test that a second call to Finalize will result in an error code + url2 = nullptr; + ASSERT_EQ(mut.Finalize(getter_AddRefs(url2)), NS_ERROR_NOT_AVAILABLE); + ASSERT_EQ(url2, nullptr); +} + +TEST(TestMozURL, MutatorErrorStatus) +{ + nsAutoCString href("http://user:pass@example.com/path?query#ref"); + RefPtr<MozURL> url; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK); + nsAutoCString out; + + // Test that trying to set the scheme to a bad value will get you an error + MozURL::Mutator mut = url->Mutate(); + mut.SetScheme("!@#$%^&*("_ns); + ASSERT_EQ(mut.GetStatus(), NS_ERROR_MALFORMED_URI); + + // Test that the mutator will not work after one faulty operation + mut.SetScheme("test"_ns); + ASSERT_EQ(mut.GetStatus(), NS_ERROR_MALFORMED_URI); +} + +TEST(TestMozURL, InitWithBase) +{ + nsAutoCString href("https://example.net/a/b.html"); + RefPtr<MozURL> url; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK); + + ASSERT_TRUE(url->Spec().EqualsLiteral("https://example.net/a/b.html")); + + RefPtr<MozURL> url2; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url2), "c.png"_ns, url), NS_OK); + + ASSERT_TRUE(url2->Spec().EqualsLiteral("https://example.net/a/c.png")); +} + +TEST(TestMozURL, Path) +{ + nsAutoCString href("about:blank"); + RefPtr<MozURL> url; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK); + + ASSERT_TRUE(url->Spec().EqualsLiteral("about:blank")); + + ASSERT_TRUE(url->Scheme().EqualsLiteral("about")); + + ASSERT_TRUE(url->FilePath().EqualsLiteral("blank")); +} + +TEST(TestMozURL, HostPort) +{ + nsAutoCString href("https://user:pass@example.net:1234/path?query#ref"); + RefPtr<MozURL> url; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK); + + ASSERT_TRUE(url->HostPort().EqualsLiteral("example.net:1234")); + + RefPtr<MozURL> url2; + url->Mutate().SetHostPort("test:321"_ns).Finalize(getter_AddRefs(url2)); + + ASSERT_TRUE(url2->HostPort().EqualsLiteral("test:321")); + ASSERT_TRUE( + url2->Spec().EqualsLiteral("https://user:pass@test:321/path?query#ref")); + + href.Assign("https://user:pass@example.net:443/path?query#ref"); + ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK); + ASSERT_TRUE(url->HostPort().EqualsLiteral("example.net")); + ASSERT_EQ(url->Port(), -1); +} + +TEST(TestMozURL, Origin) +{ + nsAutoCString href("https://user:pass@example.net:1234/path?query#ref"); + RefPtr<MozURL> url; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK); + + nsAutoCString out; + url->Origin(out); + ASSERT_TRUE(out.EqualsLiteral("https://example.net:1234")); + + RefPtr<MozURL> url2; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url2), "file:///tmp/foo"_ns), NS_OK); + url2->Origin(out); + ASSERT_TRUE(out.EqualsLiteral("file:///tmp/foo")); + + RefPtr<MozURL> url3; + ASSERT_EQ( + MozURL::Init(getter_AddRefs(url3), + nsLiteralCString( + "moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc/" + "foo/bar.html")), + NS_OK); + url3->Origin(out); + ASSERT_TRUE(out.EqualsLiteral( + "moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc")); + + RefPtr<MozURL> url4; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url4), "resource://foo/bar.html"_ns), + NS_OK); + url4->Origin(out); + ASSERT_TRUE(out.EqualsLiteral("resource://foo")); + + RefPtr<MozURL> url5; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url5), "about:home"_ns), NS_OK); + url5->Origin(out); + ASSERT_TRUE(out.EqualsLiteral("about:home")); +} + +TEST(TestMozURL, BaseDomain) +{ + nsAutoCString href("https://user:pass@example.net:1234/path?query#ref"); + RefPtr<MozURL> url; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url), href), NS_OK); + + nsAutoCString out; + ASSERT_EQ(url->BaseDomain(out), NS_OK); + ASSERT_TRUE(out.EqualsLiteral("example.net")); + + RefPtr<MozURL> url2; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url2), "file:///tmp/foo"_ns), NS_OK); + ASSERT_EQ(url2->BaseDomain(out), NS_OK); + ASSERT_TRUE(out.EqualsLiteral("/tmp/foo")); + + RefPtr<MozURL> url3; + ASSERT_EQ( + MozURL::Init(getter_AddRefs(url3), + nsLiteralCString( + "moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc/" + "foo/bar.html")), + NS_OK); + ASSERT_EQ(url3->BaseDomain(out), NS_OK); + ASSERT_TRUE(out.EqualsLiteral("53711a8f-65ed-e742-9671-1f02e267c0bc")); + + RefPtr<MozURL> url4; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url4), "resource://foo/bar.html"_ns), + NS_OK); + ASSERT_EQ(url4->BaseDomain(out), NS_OK); + ASSERT_TRUE(out.EqualsLiteral("foo")); + + RefPtr<MozURL> url5; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url5), "about:home"_ns), NS_OK); + ASSERT_EQ(url5->BaseDomain(out), NS_OK); + ASSERT_TRUE(out.EqualsLiteral("about:home")); +} + +namespace { + +bool OriginMatchesExpectedOrigin(const nsACString& aOrigin, + const nsACString& aExpectedOrigin) { + if (aExpectedOrigin.Equals("null") && + StringBeginsWith(aOrigin, "moz-nullprincipal"_ns)) { + return true; + } + return aOrigin == aExpectedOrigin; +} + +bool IsUUID(const nsACString& aString) { + if (!IsAscii(aString)) { + return false; + } + + std::regex pattern( + "^\\{[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab" + "][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}\\}$"); + return regex_match(nsCString(aString).get(), pattern); +} + +bool BaseDomainsEqual(const nsACString& aBaseDomain1, + const nsACString& aBaseDomain2) { + if (IsUUID(aBaseDomain1) && IsUUID(aBaseDomain2)) { + return true; + } + return aBaseDomain1 == aBaseDomain2; +} + +void CheckOrigin(const nsACString& aSpec, const nsACString& aBase, + const nsACString& aOrigin) { + nsCOMPtr<nsIURI> baseUri; + nsresult rv = NS_NewURI(getter_AddRefs(baseUri), aBase); + ASSERT_EQ(rv, NS_OK); + + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), aSpec, nullptr, baseUri); + ASSERT_EQ(rv, NS_OK); + + OriginAttributes attrs; + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(uri, attrs); + ASSERT_TRUE(principal); + + nsCString origin; + rv = principal->GetOriginNoSuffix(origin); + ASSERT_EQ(rv, NS_OK); + + EXPECT_TRUE(OriginMatchesExpectedOrigin(origin, aOrigin)); + + nsCString baseDomain; + rv = principal->GetBaseDomain(baseDomain); + + bool baseDomainSucceeded = NS_SUCCEEDED(rv); + + RefPtr<MozURL> baseUrl; + ASSERT_EQ(MozURL::Init(getter_AddRefs(baseUrl), aBase), NS_OK); + + RefPtr<MozURL> url; + ASSERT_EQ(MozURL::Init(getter_AddRefs(url), aSpec, baseUrl), NS_OK); + + url->Origin(origin); + + EXPECT_TRUE(OriginMatchesExpectedOrigin(origin, aOrigin)); + + nsCString baseDomain2; + rv = url->BaseDomain(baseDomain2); + + bool baseDomain2Succeeded = NS_SUCCEEDED(rv); + + EXPECT_TRUE(baseDomainSucceeded == baseDomain2Succeeded); + + if (baseDomainSucceeded) { + EXPECT_TRUE(BaseDomainsEqual(baseDomain, baseDomain2)); + } +} + +} // namespace + +TEST(TestMozURL, UrlTestData) +{ + nsCOMPtr<nsIFile> file; + nsresult rv = + NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, getter_AddRefs(file)); + ASSERT_EQ(rv, NS_OK); + + rv = file->Append(u"urltestdata.json"_ns); + ASSERT_EQ(rv, NS_OK); + + bool exists; + rv = file->Exists(&exists); + ASSERT_EQ(rv, NS_OK); + + ASSERT_TRUE(exists); + + nsCOMPtr<nsIInputStream> stream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file); + ASSERT_EQ(rv, NS_OK); + + nsCOMPtr<nsIInputStream> bufferedStream; + rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream), + stream.forget(), 4096); + ASSERT_EQ(rv, NS_OK); + + nsCString data; + rv = NS_ConsumeStream(bufferedStream, UINT32_MAX, data); + ASSERT_EQ(rv, NS_OK); + + Json::Value root; + Json::CharReaderBuilder builder; + std::unique_ptr<Json::CharReader> const reader(builder.newCharReader()); + ASSERT_TRUE( + reader->parse(data.BeginReading(), data.EndReading(), &root, nullptr)); + ASSERT_TRUE(root.isArray()); + + for (auto& item : root) { + if (!item.isObject()) { + continue; + } + + const Json::Value& skip = item["skip"]; + ASSERT_TRUE(skip.isNull() || skip.isBool()); + if (skip.isBool() && skip.asBool()) { + continue; + } + + const Json::Value& failure = item["failure"]; + ASSERT_TRUE(failure.isNull() || failure.isBool()); + if (failure.isBool() && failure.asBool()) { + continue; + } + + const Json::Value& origin = item["origin"]; + ASSERT_TRUE(origin.isNull() || origin.isString()); + if (origin.isNull()) { + continue; + } + const char* originBegin; + const char* originEnd; + origin.getString(&originBegin, &originEnd); + + const Json::Value& base = item["base"]; + ASSERT_TRUE(base.isString()); + const char* baseBegin; + const char* baseEnd; + base.getString(&baseBegin, &baseEnd); + + const Json::Value& input = item["input"]; + ASSERT_TRUE(input.isString()); + const char* inputBegin; + const char* inputEnd; + input.getString(&inputBegin, &inputEnd); + + CheckOrigin(nsDependentCString(inputBegin, inputEnd), + nsDependentCString(baseBegin, baseEnd), + nsDependentCString(originBegin, originEnd)); + } +} diff --git a/netwerk/test/gtest/TestNamedPipeService.cpp b/netwerk/test/gtest/TestNamedPipeService.cpp new file mode 100644 index 0000000000..b91a17a93e --- /dev/null +++ b/netwerk/test/gtest/TestNamedPipeService.cpp @@ -0,0 +1,281 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TestCommon.h" +#include "gtest/gtest.h" + +#include <windows.h> + +#include "mozilla/Atomics.h" +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/Monitor.h" +#include "nsNamedPipeService.h" +#include "nsNetCID.h" + +#define PIPE_NAME L"\\\\.\\pipe\\TestNPS" +#define TEST_STR "Hello World" + +using namespace mozilla; + +/** + * Unlike a monitor, an event allows a thread to wait on another thread + * completing an action without regard to ordering of the wait and the notify. + */ +class Event { + public: + explicit Event(const char* aName) : mMonitor(aName) {} + + ~Event() = default; + + void Set() { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(!mSignaled); + mSignaled = true; + mMonitor.Notify(); + } + void Wait() { + MonitorAutoLock lock(mMonitor); + while (!mSignaled) { + lock.Wait(); + } + mSignaled = false; + } + + private: + Monitor mMonitor MOZ_UNANNOTATED; + bool mSignaled = false; +}; + +class nsNamedPipeDataObserver final : public nsINamedPipeDataObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSINAMEDPIPEDATAOBSERVER + + explicit nsNamedPipeDataObserver(HANDLE aPipe); + + int Read(void* aBuffer, uint32_t aSize); + int Write(const void* aBuffer, uint32_t aSize); + + uint32_t Transferred() const { return mBytesTransferred; } + + private: + ~nsNamedPipeDataObserver() = default; + + HANDLE mPipe; + OVERLAPPED mOverlapped; + Atomic<uint32_t> mBytesTransferred; + Event mEvent; +}; + +NS_IMPL_ISUPPORTS(nsNamedPipeDataObserver, nsINamedPipeDataObserver) + +nsNamedPipeDataObserver::nsNamedPipeDataObserver(HANDLE aPipe) + : mPipe(aPipe), mOverlapped(), mBytesTransferred(0), mEvent("named-pipe") { + mOverlapped.hEvent = CreateEventA(nullptr, TRUE, TRUE, "named-pipe"); +} + +int nsNamedPipeDataObserver::Read(void* aBuffer, uint32_t aSize) { + DWORD bytesRead = 0; + if (!ReadFile(mPipe, aBuffer, aSize, &bytesRead, &mOverlapped)) { + switch (GetLastError()) { + case ERROR_IO_PENDING: { + mEvent.Wait(); + } + if (!GetOverlappedResult(mPipe, &mOverlapped, &bytesRead, FALSE)) { + ADD_FAILURE() << "GetOverlappedResult failed"; + return -1; + } + if (mBytesTransferred != bytesRead) { + ADD_FAILURE() << "GetOverlappedResult mismatch"; + return -1; + } + + break; + default: + ADD_FAILURE() << "ReadFile error " << GetLastError(); + return -1; + } + } else { + mEvent.Wait(); + + if (mBytesTransferred != bytesRead) { + ADD_FAILURE() << "GetOverlappedResult mismatch"; + return -1; + } + } + + mBytesTransferred = 0; + return bytesRead; +} + +int nsNamedPipeDataObserver::Write(const void* aBuffer, uint32_t aSize) { + DWORD bytesWritten = 0; + if (!WriteFile(mPipe, aBuffer, aSize, &bytesWritten, &mOverlapped)) { + switch (GetLastError()) { + case ERROR_IO_PENDING: { + mEvent.Wait(); + } + if (!GetOverlappedResult(mPipe, &mOverlapped, &bytesWritten, FALSE)) { + ADD_FAILURE() << "GetOverlappedResult failed"; + return -1; + } + if (mBytesTransferred != bytesWritten) { + ADD_FAILURE() << "GetOverlappedResult mismatch"; + return -1; + } + + break; + default: + ADD_FAILURE() << "WriteFile error " << GetLastError(); + return -1; + } + } else { + mEvent.Wait(); + + if (mBytesTransferred != bytesWritten) { + ADD_FAILURE() << "GetOverlappedResult mismatch"; + return -1; + } + } + + mBytesTransferred = 0; + return bytesWritten; +} + +NS_IMETHODIMP +nsNamedPipeDataObserver::OnDataAvailable(uint32_t aBytesTransferred, + void* aOverlapped) { + if (aOverlapped != &mOverlapped) { + ADD_FAILURE() << "invalid overlapped object"; + return NS_ERROR_FAILURE; + } + + DWORD bytesTransferred = 0; + BOOL ret = + GetOverlappedResult(mPipe, reinterpret_cast<LPOVERLAPPED>(aOverlapped), + &bytesTransferred, FALSE); + + if (!ret) { + ADD_FAILURE() << "GetOverlappedResult failed"; + return NS_ERROR_FAILURE; + } + + if (bytesTransferred != aBytesTransferred) { + ADD_FAILURE() << "GetOverlappedResult mismatch"; + return NS_ERROR_FAILURE; + } + + mBytesTransferred += aBytesTransferred; + mEvent.Set(); + + return NS_OK; +} + +NS_IMETHODIMP +nsNamedPipeDataObserver::OnError(uint32_t aError, void* aOverlapped) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +BOOL CreateAndConnectInstance(LPOVERLAPPED aOverlapped, LPHANDLE aPipe); +BOOL ConnectToNewClient(HANDLE aPipe, LPOVERLAPPED aOverlapped); + +BOOL CreateAndConnectInstance(LPOVERLAPPED aOverlapped, LPHANDLE aPipe) { + // FIXME: adjust parameters + *aPipe = + CreateNamedPipeW(PIPE_NAME, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, + 65536, 65536, 3000, NULL); + + if (*aPipe == INVALID_HANDLE_VALUE) { + ADD_FAILURE() << "CreateNamedPipe failed " << GetLastError(); + return FALSE; + } + + return ConnectToNewClient(*aPipe, aOverlapped); +} + +BOOL ConnectToNewClient(HANDLE aPipe, LPOVERLAPPED aOverlapped) { + if (ConnectNamedPipe(aPipe, aOverlapped)) { + ADD_FAILURE() + << "Unexpected, overlapped ConnectNamedPipe() always returns 0."; + return FALSE; + } + + switch (GetLastError()) { + case ERROR_IO_PENDING: + return TRUE; + + case ERROR_PIPE_CONNECTED: + if (SetEvent(aOverlapped->hEvent)) break; + + [[fallthrough]]; + default: // error + ADD_FAILURE() << "ConnectNamedPipe failed " << GetLastError(); + break; + } + + return FALSE; +} + +static nsresult CreateNamedPipe(LPHANDLE aServer, LPHANDLE aClient) { + OVERLAPPED overlapped; + overlapped.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL); + BOOL ret; + + ret = CreateAndConnectInstance(&overlapped, aServer); + if (!ret) { + ADD_FAILURE() << "pipe server should be pending"; + return NS_ERROR_FAILURE; + } + + *aClient = CreateFileW(PIPE_NAME, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr); + + if (*aClient == INVALID_HANDLE_VALUE) { + ADD_FAILURE() << "Unable to create pipe client"; + CloseHandle(*aServer); + return NS_ERROR_FAILURE; + } + + DWORD pipeMode = PIPE_READMODE_MESSAGE; + if (!SetNamedPipeHandleState(*aClient, &pipeMode, nullptr, nullptr)) { + ADD_FAILURE() << "SetNamedPipeHandleState error " << GetLastError(); + CloseHandle(*aServer); + CloseHandle(*aClient); + return NS_ERROR_FAILURE; + } + + WaitForSingleObjectEx(overlapped.hEvent, INFINITE, TRUE); + + return NS_OK; +} + +TEST(TestNamedPipeService, Test) +{ + nsCOMPtr<nsINamedPipeService> svc = net::NamedPipeService::GetOrCreate(); + + HANDLE readPipe, writePipe; + nsresult rv = CreateNamedPipe(&readPipe, &writePipe); + ASSERT_NS_SUCCEEDED(rv); + + RefPtr<nsNamedPipeDataObserver> readObserver = + new nsNamedPipeDataObserver(readPipe); + RefPtr<nsNamedPipeDataObserver> writeObserver = + new nsNamedPipeDataObserver(writePipe); + + ASSERT_NS_SUCCEEDED(svc->AddDataObserver(readPipe, readObserver)); + ASSERT_NS_SUCCEEDED(svc->AddDataObserver(writePipe, writeObserver)); + ASSERT_EQ(std::size_t(writeObserver->Write(TEST_STR, sizeof(TEST_STR))), + sizeof(TEST_STR)); + + char buffer[sizeof(TEST_STR)]; + ASSERT_EQ(std::size_t(readObserver->Read(buffer, sizeof(buffer))), + sizeof(TEST_STR)); + ASSERT_STREQ(buffer, TEST_STR) << "I/O mismatch"; + + ASSERT_NS_SUCCEEDED(svc->RemoveDataObserver(readPipe, readObserver)); + ASSERT_NS_SUCCEEDED(svc->RemoveDataObserver(writePipe, writeObserver)); +} diff --git a/netwerk/test/gtest/TestNetworkLinkIdHashingDarwin.cpp b/netwerk/test/gtest/TestNetworkLinkIdHashingDarwin.cpp new file mode 100644 index 0000000000..a07c9438bd --- /dev/null +++ b/netwerk/test/gtest/TestNetworkLinkIdHashingDarwin.cpp @@ -0,0 +1,93 @@ +#include <arpa/inet.h> + +#include "gtest/gtest.h" +#include "mozilla/SHA1.h" +#include "nsString.h" +#include "nsPrintfCString.h" +#include "mozilla/Logging.h" +#include "nsNetworkLinkService.h" + +using namespace mozilla; + +in6_addr StringToSockAddr(const std::string& str) { + sockaddr_in6 ip; + inet_pton(AF_INET6, str.c_str(), &(ip.sin6_addr)); + return ip.sin6_addr; +} + +TEST(TestNetworkLinkIdHashingDarwin, Single) +{ + // Setup + SHA1Sum expected_sha1; + SHA1Sum::Hash expected_digest; + + in6_addr a1 = StringToSockAddr("2001:db8:8714:3a91::1"); + + // Prefix + expected_sha1.update(&a1, sizeof(in6_addr)); + // Netmask + expected_sha1.update(&a1, sizeof(in6_addr)); + expected_sha1.finish(expected_digest); + + std::vector<prefix_and_netmask> prefixNetmaskStore; + prefixNetmaskStore.push_back(std::make_pair(a1, a1)); + SHA1Sum actual_sha1; + // Run + nsNetworkLinkService::HashSortedPrefixesAndNetmasks(prefixNetmaskStore, + &actual_sha1); + SHA1Sum::Hash actual_digest; + actual_sha1.finish(actual_digest); + + // Assert + ASSERT_EQ(0, memcmp(&expected_digest, &actual_digest, sizeof(SHA1Sum::Hash))); +} + +TEST(TestNetworkLinkIdHashingDarwin, Multiple) +{ + // Setup + SHA1Sum expected_sha1; + SHA1Sum::Hash expected_digest; + + std::vector<in6_addr> addresses; + addresses.push_back(StringToSockAddr("2001:db8:8714:3a91::1")); + addresses.push_back(StringToSockAddr("2001:db8:8714:3a91::2")); + addresses.push_back(StringToSockAddr("2001:db8:8714:3a91::3")); + addresses.push_back(StringToSockAddr("2001:db8:8714:3a91::4")); + + for (const auto& address : addresses) { + // Prefix + expected_sha1.update(&address, sizeof(in6_addr)); + // Netmask + expected_sha1.update(&address, sizeof(in6_addr)); + } + expected_sha1.finish(expected_digest); + + // Ordered + std::vector<prefix_and_netmask> ordered; + for (const auto& address : addresses) { + ordered.push_back(std::make_pair(address, address)); + } + SHA1Sum ordered_sha1; + + // Unordered + std::vector<prefix_and_netmask> reversed; + for (auto it = addresses.rbegin(); it != addresses.rend(); ++it) { + reversed.push_back(std::make_pair(*it, *it)); + } + SHA1Sum reversed_sha1; + + // Run + nsNetworkLinkService::HashSortedPrefixesAndNetmasks(ordered, &ordered_sha1); + SHA1Sum::Hash ordered_digest; + ordered_sha1.finish(ordered_digest); + + nsNetworkLinkService::HashSortedPrefixesAndNetmasks(reversed, &reversed_sha1); + SHA1Sum::Hash reversed_digest; + reversed_sha1.finish(reversed_digest); + + // Assert + ASSERT_EQ(0, + memcmp(&expected_digest, &ordered_digest, sizeof(SHA1Sum::Hash))); + ASSERT_EQ(0, + memcmp(&expected_digest, &reversed_digest, sizeof(SHA1Sum::Hash))); +} diff --git a/netwerk/test/gtest/TestNetworkLinkIdHashingWindows.cpp b/netwerk/test/gtest/TestNetworkLinkIdHashingWindows.cpp new file mode 100644 index 0000000000..eb2097d0a4 --- /dev/null +++ b/netwerk/test/gtest/TestNetworkLinkIdHashingWindows.cpp @@ -0,0 +1,88 @@ +#include <combaseapi.h> + +#include "gtest/gtest.h" +#include "mozilla/SHA1.h" +#include "nsNotifyAddrListener.h" + +using namespace mozilla; + +GUID StringToGuid(const std::string& str) { + GUID guid; + sscanf(str.c_str(), + "%8lx-%4hx-%4hx-%2hhx%2hhx-%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx", + &guid.Data1, &guid.Data2, &guid.Data3, &guid.Data4[0], &guid.Data4[1], + &guid.Data4[2], &guid.Data4[3], &guid.Data4[4], &guid.Data4[5], + &guid.Data4[6], &guid.Data4[7]); + + return guid; +} + +TEST(TestGuidHashWindows, Single) +{ + // Setup + SHA1Sum expected_sha1; + SHA1Sum::Hash expected_digest; + + GUID g1 = StringToGuid("264555b1-289c-4494-83d1-e158d1d95115"); + + expected_sha1.update(&g1, sizeof(GUID)); + expected_sha1.finish(expected_digest); + + std::vector<GUID> nwGUIDS; + nwGUIDS.push_back(g1); + SHA1Sum actual_sha1; + // Run + nsNotifyAddrListener::HashSortedNetworkIds(nwGUIDS, actual_sha1); + SHA1Sum::Hash actual_digest; + actual_sha1.finish(actual_digest); + + // Assert + ASSERT_EQ(0, memcmp(&expected_digest, &actual_digest, sizeof(SHA1Sum::Hash))); +} + +TEST(TestNetworkLinkIdHashingWindows, Multiple) +{ + // Setup + SHA1Sum expected_sha1; + SHA1Sum::Hash expected_digest; + + std::vector<GUID> nwGUIDS; + nwGUIDS.push_back(StringToGuid("00000000-0000-0000-0000-000000000001")); + nwGUIDS.push_back(StringToGuid("00000000-0000-0000-0000-000000000002")); + nwGUIDS.push_back(StringToGuid("00000000-0000-0000-0000-000000000003")); + nwGUIDS.push_back(StringToGuid("00000000-0000-0000-0000-000000000004")); + + for (const auto& guid : nwGUIDS) { + expected_sha1.update(&guid, sizeof(GUID)); + } + expected_sha1.finish(expected_digest); + + // Ordered + std::vector<GUID> ordered; + for (const auto& guid : nwGUIDS) { + ordered.push_back(guid); + } + SHA1Sum ordered_sha1; + + // Unordered + std::vector<GUID> reversed; + for (auto it = nwGUIDS.rbegin(); it != nwGUIDS.rend(); ++it) { + reversed.push_back(*it); + } + SHA1Sum reversed_sha1; + + // Run + nsNotifyAddrListener::HashSortedNetworkIds(ordered, ordered_sha1); + SHA1Sum::Hash ordered_digest; + ordered_sha1.finish(ordered_digest); + + nsNotifyAddrListener::HashSortedNetworkIds(reversed, reversed_sha1); + SHA1Sum::Hash reversed_digest; + reversed_sha1.finish(reversed_digest); + + // Assert + ASSERT_EQ(0, + memcmp(&expected_digest, &ordered_digest, sizeof(SHA1Sum::Hash))); + ASSERT_EQ(0, + memcmp(&expected_digest, &reversed_digest, sizeof(SHA1Sum::Hash))); +} diff --git a/netwerk/test/gtest/TestPACMan.cpp b/netwerk/test/gtest/TestPACMan.cpp new file mode 100644 index 0000000000..46c57cfc79 --- /dev/null +++ b/netwerk/test/gtest/TestPACMan.cpp @@ -0,0 +1,246 @@ +#include <utility> + +#include "gtest/gtest.h" +#include "nsServiceManagerUtils.h" +#include "../../../xpcom/threads/nsThreadManager.h" +#include "nsIDHCPClient.h" +#include "nsIPrefBranch.h" +#include "nsComponentManager.h" +#include "nsIPrefService.h" +#include "nsNetCID.h" +#include "mozilla/ModuleUtils.h" +#include "mozilla/GenericFactory.h" +#include "../../base/nsPACMan.h" + +#define TEST_WPAD_DHCP_OPTION "http://pac/pac.dat" +#define TEST_ASSIGNED_PAC_URL "http://assignedpac/pac.dat" +#define WPAD_PREF 4 +#define NETWORK_PROXY_TYPE_PREF_NAME "network.proxy.type" +#define GETTING_NETWORK_PROXY_TYPE_FAILED (-1) + +nsCString WPADOptionResult; + +namespace mozilla { +namespace net { + +nsresult SetNetworkProxyType(int32_t pref) { + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + + if (!prefs) { + return NS_ERROR_FACTORY_NOT_REGISTERED; + } + return prefs->SetIntPref(NETWORK_PROXY_TYPE_PREF_NAME, pref); +} + +nsresult GetNetworkProxyType(int32_t* pref) { + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + + if (!prefs) { + return NS_ERROR_FACTORY_NOT_REGISTERED; + } + return prefs->GetIntPref(NETWORK_PROXY_TYPE_PREF_NAME, pref); +} + +class nsTestDHCPClient final : public nsIDHCPClient { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIDHCPCLIENT + + nsTestDHCPClient() = default; + + nsresult Init() { return NS_OK; }; + + private: + ~nsTestDHCPClient() = default; +}; + +NS_IMETHODIMP +nsTestDHCPClient::GetOption(uint8_t option, nsACString& _retval) { + _retval.Assign(WPADOptionResult); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsTestDHCPClient, nsIDHCPClient) + +#define NS_TESTDHCPCLIENTSERVICE_CID /* {FEBF1D69-4D7D-4891-9524-045AD18B5593} \ + */ \ + { \ + 0xFEBF1D69, 0x4D7D, 0x4891, { \ + 0x95, 0x24, 0x04, 0x5a, 0xd1, 0x8b, 0x55, 0x93 \ + } \ + } + +NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsTestDHCPClient, Init) +NS_DEFINE_NAMED_CID(NS_TESTDHCPCLIENTSERVICE_CID); + +void SetOptionResult(const char* result) { WPADOptionResult.Assign(result); } + +class ProcessPendingEventsAction final : public Runnable { + public: + ProcessPendingEventsAction() : Runnable("net::ProcessPendingEventsAction") {} + + NS_IMETHOD + Run() override { + if (NS_HasPendingEvents(nullptr)) { + NS_WARNING("Found pending requests on PAC thread"); + nsresult rv; + rv = NS_ProcessPendingEvents(nullptr); + EXPECT_EQ(NS_OK, rv); + } + NS_WARNING("No pending requests on PAC thread"); + return NS_OK; + } +}; + +class TestPACMan : public ::testing::Test { + protected: + RefPtr<nsPACMan> mPACMan; + + void ProcessAllEvents() { + ProcessPendingEventsOnPACThread(); + nsresult rv; + while (NS_HasPendingEvents(nullptr)) { + NS_WARNING("Pending events on main thread"); + rv = NS_ProcessPendingEvents(nullptr); + ASSERT_EQ(NS_OK, rv); + ProcessPendingEventsOnPACThread(); + } + NS_WARNING("End of pending events on main thread"); + } + + // This method is used to ensure that all pending events on the main thread + // and the Proxy thread are processsed. + // It iterates over ProcessAllEvents because simply calling ProcessAllEvents + // once did not reliably process the events on both threads on all platforms. + void ProcessAllEventsTenTimes() { + for (int i = 0; i < 10; i++) { + ProcessAllEvents(); + } + } + + virtual void SetUp() { + ASSERT_EQ(NS_OK, GetNetworkProxyType(&originalNetworkProxyTypePref)); + nsCOMPtr<nsIFactory> factory; + nsresult rv = nsComponentManagerImpl::gComponentManager->GetClassObject( + kNS_TESTDHCPCLIENTSERVICE_CID, NS_GET_IID(nsIFactory), + getter_AddRefs(factory)); + if (NS_SUCCEEDED(rv) && factory) { + rv = nsComponentManagerImpl::gComponentManager->UnregisterFactory( + kNS_TESTDHCPCLIENTSERVICE_CID, factory); + ASSERT_EQ(NS_OK, rv); + } + factory = new mozilla::GenericFactory(nsTestDHCPClientConstructor); + nsComponentManagerImpl::gComponentManager->RegisterFactory( + kNS_TESTDHCPCLIENTSERVICE_CID, "nsTestDHCPClient", + NS_DHCPCLIENT_CONTRACTID, factory); + + mPACMan = new nsPACMan(nullptr); + mPACMan->SetWPADOverDHCPEnabled(true); + mPACMan->Init(nullptr); + ASSERT_EQ(NS_OK, SetNetworkProxyType(WPAD_PREF)); + } + + virtual void TearDown() { + mPACMan->Shutdown(); + if (originalNetworkProxyTypePref != GETTING_NETWORK_PROXY_TYPE_FAILED) { + ASSERT_EQ(NS_OK, SetNetworkProxyType(originalNetworkProxyTypePref)); + } + } + + nsCOMPtr<nsIDHCPClient> GetPACManDHCPCient() { return mPACMan->mDHCPClient; } + + void SetPACManDHCPCient(nsCOMPtr<nsIDHCPClient> aValue) { + mPACMan->mDHCPClient = std::move(aValue); + } + + void AssertPACSpecEqualTo(const char* aExpected) { + ASSERT_STREQ(aExpected, mPACMan->mPACURISpec.Data()); + } + + private: + int32_t originalNetworkProxyTypePref = GETTING_NETWORK_PROXY_TYPE_FAILED; + + void ProcessPendingEventsOnPACThread() { + RefPtr<ProcessPendingEventsAction> action = + new ProcessPendingEventsAction(); + + mPACMan->DispatchToPAC(action.forget(), /*aSync =*/true); + } +}; + +TEST_F(TestPACMan, TestCreateDHCPClientAndGetOption) { + SetOptionResult(TEST_WPAD_DHCP_OPTION); + nsCString spec; + + GetPACManDHCPCient()->GetOption(252, spec); + + ASSERT_STREQ(TEST_WPAD_DHCP_OPTION, spec.Data()); +} + +TEST_F(TestPACMan, TestCreateDHCPClientAndGetEmptyOption) { + SetOptionResult(""); + nsCString spec; + spec.AssignLiteral(TEST_ASSIGNED_PAC_URL); + + GetPACManDHCPCient()->GetOption(252, spec); + + ASSERT_TRUE(spec.IsEmpty()); +} + +TEST_F(TestPACMan, + WhenTheDHCPClientExistsAndDHCPIsNonEmptyDHCPOptionIsUsedAsPACUri) { + SetOptionResult(TEST_WPAD_DHCP_OPTION); + + mPACMan->LoadPACFromURI(""_ns); + ProcessAllEventsTenTimes(); + + ASSERT_STREQ(TEST_WPAD_DHCP_OPTION, WPADOptionResult.Data()); + AssertPACSpecEqualTo(TEST_WPAD_DHCP_OPTION); +} + +TEST_F(TestPACMan, WhenTheDHCPResponseIsEmptyWPADDefaultsToStandardURL) { + SetOptionResult(""_ns.Data()); + + mPACMan->LoadPACFromURI(""_ns); + ASSERT_TRUE(NS_HasPendingEvents(nullptr)); + ProcessAllEventsTenTimes(); + + ASSERT_STREQ("", WPADOptionResult.Data()); + AssertPACSpecEqualTo("http://wpad/wpad.dat"); +} + +TEST_F(TestPACMan, WhenThereIsNoDHCPClientWPADDefaultsToStandardURL) { + SetOptionResult(TEST_WPAD_DHCP_OPTION); + SetPACManDHCPCient(nullptr); + + mPACMan->LoadPACFromURI(""_ns); + ProcessAllEventsTenTimes(); + + ASSERT_STREQ(TEST_WPAD_DHCP_OPTION, WPADOptionResult.Data()); + AssertPACSpecEqualTo("http://wpad/wpad.dat"); +} + +TEST_F(TestPACMan, WhenWPADOverDHCPIsPreffedOffWPADDefaultsToStandardURL) { + SetOptionResult(TEST_WPAD_DHCP_OPTION); + mPACMan->SetWPADOverDHCPEnabled(false); + + mPACMan->LoadPACFromURI(""_ns); + ProcessAllEventsTenTimes(); + + ASSERT_STREQ(TEST_WPAD_DHCP_OPTION, WPADOptionResult.Data()); + AssertPACSpecEqualTo("http://wpad/wpad.dat"); +} + +TEST_F(TestPACMan, WhenPACUriIsSetDirectlyItIsUsedRatherThanWPAD) { + SetOptionResult(TEST_WPAD_DHCP_OPTION); + nsCString spec; + spec.AssignLiteral(TEST_ASSIGNED_PAC_URL); + + mPACMan->LoadPACFromURI(spec); + ProcessAllEventsTenTimes(); + + AssertPACSpecEqualTo(TEST_ASSIGNED_PAC_URL); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/test/gtest/TestProtocolProxyService.cpp b/netwerk/test/gtest/TestProtocolProxyService.cpp new file mode 100644 index 0000000000..a26f5f62a8 --- /dev/null +++ b/netwerk/test/gtest/TestProtocolProxyService.cpp @@ -0,0 +1,164 @@ +#include "gtest/gtest.h" + +#include "nsCOMPtr.h" +#include "nsNetCID.h" +#include "nsString.h" +#include "nsComponentManagerUtils.h" +#include "../../base/nsProtocolProxyService.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Preferences.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace net { + +TEST(TestProtocolProxyService, LoadHostFilters) +{ + nsCOMPtr<nsIProtocolProxyService2> ps = + do_GetService(NS_PROTOCOLPROXYSERVICE_CID); + ASSERT_TRUE(ps); + mozilla::net::nsProtocolProxyService* pps = + static_cast<mozilla::net::nsProtocolProxyService*>(ps.get()); + + nsCOMPtr<nsIURI> url; + nsAutoCString spec; + + auto CheckLoopbackURLs = [&](bool expected) { + // loopback IPs are always filtered + spec = "http://127.0.0.1"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + spec = "http://[::1]"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + spec = "http://localhost"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + }; + + auto CheckURLs = [&](bool expected) { + spec = "http://example.com"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + + spec = "https://10.2.3.4"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 443), expected); + + spec = "http://1.2.3.4"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + + spec = "http://1.2.3.4:8080"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + + spec = "http://[2001::1]"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + + spec = "http://2.3.4.5:7777"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + + spec = "http://[abcd::2]:123"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + + spec = "http://bla.test.com"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + }; + + auto CheckPortDomain = [&](bool expected) { + spec = "http://blabla.com:10"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + }; + + auto CheckLocalDomain = [&](bool expected) { + spec = "http://test"; + ASSERT_EQ(NS_NewURI(getter_AddRefs(url), spec), NS_OK); + ASSERT_EQ(pps->CanUseProxy(url, 80), expected); + }; + + // -------------------------------------------------------------------------- + + nsAutoCString filter; + + // Anything is allowed when there are no filters set + printf("Testing empty filter: %s\n", filter.get()); + pps->LoadHostFilters(filter); + + CheckLoopbackURLs(false); + CheckLocalDomain(true); + CheckURLs(true); + CheckPortDomain(true); + + // -------------------------------------------------------------------------- + + filter = + "example.com, 1.2.3.4/16, [2001::1], 10.0.0.0/8, 2.3.0.0/16:7777, " + "[abcd::1]/64:123, *.test.com"; + printf("Testing filter: %s\n", filter.get()); + pps->LoadHostFilters(filter); + + CheckLoopbackURLs(false); + // Check URLs can no longer use filtered proxy + CheckURLs(false); + CheckLocalDomain(true); + CheckPortDomain(true); + + // -------------------------------------------------------------------------- + + // This is space separated. See bug 1346711 comment 4. We check this to keep + // backwards compatibility. + filter = "<local> blabla.com:10"; + printf("Testing filter: %s\n", filter.get()); + pps->LoadHostFilters(filter); + + CheckLoopbackURLs(false); + CheckURLs(true); + CheckLocalDomain(false); + CheckPortDomain(false); + + // Check that we don't crash on weird input + filter = "a b c abc:1x2, ,, * ** *.* *:10 :20 :40/12 */12:90"; + printf("Testing filter: %s\n", filter.get()); + pps->LoadHostFilters(filter); + + // Check that filtering works properly when the filter is set to "<local>" + filter = "<local>"; + printf("Testing filter: %s\n", filter.get()); + pps->LoadHostFilters(filter); + + CheckLoopbackURLs(false); + CheckURLs(true); + CheckLocalDomain(false); + CheckPortDomain(true); + + // Check that allow_hijacking_localhost works with empty filter + Preferences::SetBool("network.proxy.allow_hijacking_localhost", true); + + filter = ""; + printf("Testing filter: %s\n", filter.get()); + pps->LoadHostFilters(filter); + + CheckLoopbackURLs(true); + CheckLocalDomain(true); + CheckURLs(true); + CheckPortDomain(true); + + // Check that allow_hijacking_localhost works with non-trivial filter + filter = "127.0.0.1, [::1], localhost, blabla.com:10"; + printf("Testing filter: %s\n", filter.get()); + pps->LoadHostFilters(filter); + + CheckLoopbackURLs(false); + CheckLocalDomain(true); + CheckURLs(true); + CheckPortDomain(false); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/test/gtest/TestReadStreamToString.cpp b/netwerk/test/gtest/TestReadStreamToString.cpp new file mode 100644 index 0000000000..f5fa0a4979 --- /dev/null +++ b/netwerk/test/gtest/TestReadStreamToString.cpp @@ -0,0 +1,190 @@ +#include "gtest/gtest.h" + +#include "Helpers.h" +#include "nsCOMPtr.h" +#include "nsNetUtil.h" +#include "nsStringStream.h" + +// Here we test the reading a pre-allocated size +TEST(TestReadStreamToString, SyncStreamPreAllocatedSize) +{ + nsCString buffer; + buffer.AssignLiteral("Hello world!"); + + nsCOMPtr<nsIInputStream> stream; + ASSERT_EQ(NS_OK, NS_NewCStringInputStream(getter_AddRefs(stream), buffer)); + + uint64_t written; + nsAutoCString result; + result.SetLength(5); + + void* ptr = result.BeginWriting(); + + ASSERT_EQ(NS_OK, NS_ReadInputStreamToString(stream, result, 5, &written)); + ASSERT_EQ((uint64_t)5, written); + ASSERT_TRUE(nsCString(buffer.get(), 5).Equals(result)); + + // The pointer should be equal: no relocation. + ASSERT_EQ(ptr, result.BeginWriting()); +} + +// Here we test the reading the full size of a sync stream +TEST(TestReadStreamToString, SyncStreamFullSize) +{ + nsCString buffer; + buffer.AssignLiteral("Hello world!"); + + nsCOMPtr<nsIInputStream> stream; + ASSERT_EQ(NS_OK, NS_NewCStringInputStream(getter_AddRefs(stream), buffer)); + + uint64_t written; + nsAutoCString result; + + ASSERT_EQ(NS_OK, NS_ReadInputStreamToString(stream, result, buffer.Length(), + &written)); + ASSERT_EQ(buffer.Length(), written); + ASSERT_TRUE(buffer.Equals(result)); +} + +// Here we test the reading less than the full size of a sync stream +TEST(TestReadStreamToString, SyncStreamLessThan) +{ + nsCString buffer; + buffer.AssignLiteral("Hello world!"); + + nsCOMPtr<nsIInputStream> stream; + ASSERT_EQ(NS_OK, NS_NewCStringInputStream(getter_AddRefs(stream), buffer)); + + uint64_t written; + nsAutoCString result; + + ASSERT_EQ(NS_OK, NS_ReadInputStreamToString(stream, result, 5, &written)); + ASSERT_EQ((uint64_t)5, written); + ASSERT_TRUE(nsCString(buffer.get(), 5).Equals(result)); +} + +// Here we test the reading more than the full size of a sync stream +TEST(TestReadStreamToString, SyncStreamMoreThan) +{ + nsCString buffer; + buffer.AssignLiteral("Hello world!"); + + nsCOMPtr<nsIInputStream> stream; + ASSERT_EQ(NS_OK, NS_NewCStringInputStream(getter_AddRefs(stream), buffer)); + + uint64_t written; + nsAutoCString result; + + // Reading more than the buffer size. + ASSERT_EQ(NS_OK, NS_ReadInputStreamToString(stream, result, + buffer.Length() + 5, &written)); + ASSERT_EQ(buffer.Length(), written); + ASSERT_TRUE(buffer.Equals(result)); +} + +// Here we test the reading a sync stream without passing the size +TEST(TestReadStreamToString, SyncStreamUnknownSize) +{ + nsCString buffer; + buffer.AssignLiteral("Hello world!"); + + nsCOMPtr<nsIInputStream> stream; + ASSERT_EQ(NS_OK, NS_NewCStringInputStream(getter_AddRefs(stream), buffer)); + + uint64_t written; + nsAutoCString result; + + // Reading all without passing the size + ASSERT_EQ(NS_OK, NS_ReadInputStreamToString(stream, result, -1, &written)); + ASSERT_EQ(buffer.Length(), written); + ASSERT_TRUE(buffer.Equals(result)); +} + +// Here we test the reading the full size of an async stream +TEST(TestReadStreamToString, AsyncStreamFullSize) +{ + nsCString buffer; + buffer.AssignLiteral("Hello world!"); + + nsCOMPtr<nsIInputStream> stream = new testing::AsyncStringStream(buffer); + + uint64_t written; + nsAutoCString result; + + ASSERT_EQ(NS_OK, NS_ReadInputStreamToString(stream, result, buffer.Length(), + &written)); + ASSERT_EQ(buffer.Length(), written); + ASSERT_TRUE(buffer.Equals(result)); +} + +// Here we test the reading less than the full size of an async stream +TEST(TestReadStreamToString, AsyncStreamLessThan) +{ + nsCString buffer; + buffer.AssignLiteral("Hello world!"); + + nsCOMPtr<nsIInputStream> stream = new testing::AsyncStringStream(buffer); + + uint64_t written; + nsAutoCString result; + + ASSERT_EQ(NS_OK, NS_ReadInputStreamToString(stream, result, 5, &written)); + ASSERT_EQ((uint64_t)5, written); + ASSERT_TRUE(nsCString(buffer.get(), 5).Equals(result)); +} + +// Here we test the reading more than the full size of an async stream +TEST(TestReadStreamToString, AsyncStreamMoreThan) +{ + nsCString buffer; + buffer.AssignLiteral("Hello world!"); + + nsCOMPtr<nsIInputStream> stream = new testing::AsyncStringStream(buffer); + + uint64_t written; + nsAutoCString result; + + // Reading more than the buffer size. + ASSERT_EQ(NS_OK, NS_ReadInputStreamToString(stream, result, + buffer.Length() + 5, &written)); + ASSERT_EQ(buffer.Length(), written); + ASSERT_TRUE(buffer.Equals(result)); +} + +// Here we test the reading an async stream without passing the size +TEST(TestReadStreamToString, AsyncStreamUnknownSize) +{ + nsCString buffer; + buffer.AssignLiteral("Hello world!"); + + nsCOMPtr<nsIInputStream> stream = new testing::AsyncStringStream(buffer); + + uint64_t written; + nsAutoCString result; + + // Reading all without passing the size + ASSERT_EQ(NS_OK, NS_ReadInputStreamToString(stream, result, -1, &written)); + ASSERT_EQ(buffer.Length(), written); + ASSERT_TRUE(buffer.Equals(result)); +} + +// Here we test the reading an async big stream without passing the size +TEST(TestReadStreamToString, AsyncStreamUnknownBigSize) +{ + nsCString buffer; + + buffer.SetLength(4096 * 2); + for (uint32_t i = 0; i < 4096 * 2; ++i) { + buffer.BeginWriting()[i] = i % 10; + } + + nsCOMPtr<nsIInputStream> stream = new testing::AsyncStringStream(buffer); + + uint64_t written; + nsAutoCString result; + + // Reading all without passing the size + ASSERT_EQ(NS_OK, NS_ReadInputStreamToString(stream, result, -1, &written)); + ASSERT_EQ(buffer.Length(), written); + ASSERT_TRUE(buffer.Equals(result)); +} diff --git a/netwerk/test/gtest/TestSSLTokensCache.cpp b/netwerk/test/gtest/TestSSLTokensCache.cpp new file mode 100644 index 0000000000..3ef9485462 --- /dev/null +++ b/netwerk/test/gtest/TestSSLTokensCache.cpp @@ -0,0 +1,168 @@ +#include <numeric> + +#include "CertVerifier.h" +#include "CommonSocketControl.h" +#include "SSLTokensCache.h" +#include "TransportSecurityInfo.h" +#include "gtest/gtest.h" +#include "mozilla/Preferences.h" +#include "nsITransportSecurityInfo.h" +#include "nsIWebProgressListener.h" +#include "nsIX509Cert.h" +#include "nsIX509CertDB.h" +#include "nsServiceManagerUtils.h" +#include "sslproto.h" + +static already_AddRefed<CommonSocketControl> createDummySocketControl() { + nsCOMPtr<nsIX509CertDB> certDB(do_GetService(NS_X509CERTDB_CONTRACTID)); + EXPECT_TRUE(certDB); + nsLiteralCString base64( + "MIIBbjCCARWgAwIBAgIUOyCxVVqw03yUxKSfSojsMF8K/" + "ikwCgYIKoZIzj0EAwIwHTEbMBkGA1UEAwwScm9vdF9zZWNwMjU2azFfMjU2MCIYDzIwMjAxM" + "TI3MDAwMDAwWhgPMjAyMzAyMDUwMDAwMDBaMC8xLTArBgNVBAMMJGludF9zZWNwMjU2cjFfM" + "jU2LXJvb3Rfc2VjcDI1NmsxXzI1NjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABE+/" + "u7th4Pj5saYKWayHBOLsBQtCPjz3LpI/" + "LE95S0VcKmnSM0VsNsQRnQcG4A7tyNGTkNeZG3stB6ME6qBKpsCjHTAbMAwGA1UdEwQFMAMB" + "Af8wCwYDVR0PBAQDAgEGMAoGCCqGSM49BAMCA0cAMEQCIFuwodUwyOUnIR4KN5ZCSrU7y4iz" + "4/1EWRdHm5kWKi8dAiB6Ixn9sw3uBVbyxnQKYqGnOwM+qLOkJK0W8XkIE3n5sg=="); + nsCOMPtr<nsIX509Cert> cert; + EXPECT_TRUE(NS_SUCCEEDED( + certDB->ConstructX509FromBase64(base64, getter_AddRefs(cert)))); + EXPECT_TRUE(cert); + nsTArray<nsTArray<uint8_t>> succeededCertChain; + for (size_t i = 0; i < 3; i++) { + nsTArray<uint8_t> certDER; + EXPECT_TRUE(NS_SUCCEEDED(cert->GetRawDER(certDER))); + succeededCertChain.AppendElement(std::move(certDER)); + } + RefPtr<CommonSocketControl> socketControl( + new CommonSocketControl(nsLiteralCString("example.com"), 433, 0)); + socketControl->SetServerCert(cert, mozilla::psm::EVStatus::NotEV); + socketControl->SetSucceededCertChain(std::move(succeededCertChain)); + return socketControl.forget(); +} + +static auto MakeTestData(const size_t aDataSize) { + auto data = nsTArray<uint8_t>(); + data.SetLength(aDataSize); + std::iota(data.begin(), data.end(), 0); + return data; +} + +static void putToken(const nsACString& aKey, uint32_t aSize) { + RefPtr<CommonSocketControl> socketControl = createDummySocketControl(); + nsTArray<uint8_t> token = MakeTestData(aSize); + nsresult rv = mozilla::net::SSLTokensCache::Put(aKey, token.Elements(), aSize, + socketControl, aSize); + ASSERT_EQ(rv, NS_OK); +} + +static void getAndCheckResult(const nsACString& aKey, uint32_t aExpectedSize) { + nsTArray<uint8_t> result; + mozilla::net::SessionCacheInfo unused; + nsresult rv = mozilla::net::SSLTokensCache::Get(aKey, result, unused); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(result.Length(), (size_t)aExpectedSize); +} + +TEST(TestTokensCache, SinglePut) +{ + mozilla::net::SSLTokensCache::Clear(); + mozilla::Preferences::SetInt("network.ssl_tokens_cache_records_per_entry", 1); + mozilla::Preferences::SetBool("network.ssl_tokens_cache_use_only_once", + false); + + putToken("anon:www.example.com:443"_ns, 100); + nsTArray<uint8_t> result; + mozilla::net::SessionCacheInfo unused; + uint64_t id = 0; + nsresult rv = mozilla::net::SSLTokensCache::Get("anon:www.example.com:443"_ns, + result, unused, &id); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(result.Length(), (size_t)100); + ASSERT_EQ(id, (uint64_t)1); + rv = mozilla::net::SSLTokensCache::Get("anon:www.example.com:443"_ns, result, + unused, &id); + ASSERT_EQ(rv, NS_OK); + + mozilla::Preferences::SetBool("network.ssl_tokens_cache_use_only_once", true); + // network.ssl_tokens_cache_use_only_once is true, so the record will be + // removed after SSLTokensCache::Get below. + rv = mozilla::net::SSLTokensCache::Get("anon:www.example.com:443"_ns, result, + unused); + ASSERT_EQ(rv, NS_OK); + rv = mozilla::net::SSLTokensCache::Get("anon:www.example.com:443"_ns, result, + unused); + ASSERT_EQ(rv, NS_ERROR_NOT_AVAILABLE); +} + +TEST(TestTokensCache, MultiplePut) +{ + mozilla::net::SSLTokensCache::Clear(); + mozilla::Preferences::SetInt("network.ssl_tokens_cache_records_per_entry", 3); + + putToken("anon:www.example1.com:443"_ns, 300); + // This record will be removed because + // "network.ssl_tokens_cache_records_per_entry" is 3. + putToken("anon:www.example1.com:443"_ns, 100); + putToken("anon:www.example1.com:443"_ns, 200); + putToken("anon:www.example1.com:443"_ns, 400); + + // Test if records are ordered by the expiration time + getAndCheckResult("anon:www.example1.com:443"_ns, 200); + getAndCheckResult("anon:www.example1.com:443"_ns, 300); + getAndCheckResult("anon:www.example1.com:443"_ns, 400); +} + +TEST(TestTokensCache, RemoveAll) +{ + mozilla::net::SSLTokensCache::Clear(); + mozilla::Preferences::SetInt("network.ssl_tokens_cache_records_per_entry", 3); + + putToken("anon:www.example1.com:443"_ns, 100); + putToken("anon:www.example1.com:443"_ns, 200); + putToken("anon:www.example1.com:443"_ns, 300); + + putToken("anon:www.example2.com:443"_ns, 100); + putToken("anon:www.example2.com:443"_ns, 200); + putToken("anon:www.example2.com:443"_ns, 300); + + nsTArray<uint8_t> result; + mozilla::net::SessionCacheInfo unused; + nsresult rv = mozilla::net::SSLTokensCache::Get( + "anon:www.example1.com:443"_ns, result, unused); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(result.Length(), (size_t)100); + + rv = mozilla::net::SSLTokensCache::RemoveAll("anon:www.example1.com:443"_ns); + ASSERT_EQ(rv, NS_OK); + + rv = mozilla::net::SSLTokensCache::Get("anon:www.example1.com:443"_ns, result, + unused); + ASSERT_EQ(rv, NS_ERROR_NOT_AVAILABLE); + + rv = mozilla::net::SSLTokensCache::Get("anon:www.example2.com:443"_ns, result, + unused); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(result.Length(), (size_t)100); +} + +TEST(TestTokensCache, Eviction) +{ + mozilla::net::SSLTokensCache::Clear(); + + mozilla::Preferences::SetInt("network.ssl_tokens_cache_records_per_entry", 3); + mozilla::Preferences::SetInt("network.ssl_tokens_cache_capacity", 8); + + putToken("anon:www.example2.com:443"_ns, 300); + putToken("anon:www.example2.com:443"_ns, 400); + putToken("anon:www.example2.com:443"_ns, 500); + // The one has expiration time "300" will be removed because we only allow 3 + // records per entry. + putToken("anon:www.example2.com:443"_ns, 600); + + putToken("anon:www.example3.com:443"_ns, 600); + putToken("anon:www.example3.com:443"_ns, 500); + // The one has expiration time "400" was evicted, so we get "500". + getAndCheckResult("anon:www.example2.com:443"_ns, 500); +} diff --git a/netwerk/test/gtest/TestServerTimingHeader.cpp b/netwerk/test/gtest/TestServerTimingHeader.cpp new file mode 100644 index 0000000000..183726a440 --- /dev/null +++ b/netwerk/test/gtest/TestServerTimingHeader.cpp @@ -0,0 +1,238 @@ +#include "gtest/gtest.h" + +#include "mozilla/Unused.h" +#include "mozilla/net/nsServerTiming.h" +#include <string> +#include <vector> + +using namespace mozilla; +using namespace mozilla::net; + +void testServerTimingHeader( + const char* headerValue, + std::vector<std::vector<std::string>> expectedResults) { + nsAutoCString header(headerValue); + ServerTimingParser parser(header); + parser.Parse(); + + nsTArray<nsCOMPtr<nsIServerTiming>> results = + parser.TakeServerTimingHeaders(); + + ASSERT_EQ(results.Length(), expectedResults.size()); + + unsigned i = 0; + for (const auto& header : results) { + std::vector<std::string> expectedResult(expectedResults[i++]); + nsCString name; + mozilla::Unused << header->GetName(name); + ASSERT_TRUE(name.Equals(expectedResult[0].c_str())); + + double duration; + mozilla::Unused << header->GetDuration(&duration); + ASSERT_EQ(duration, atof(expectedResult[1].c_str())); + + nsCString description; + mozilla::Unused << header->GetDescription(description); + ASSERT_TRUE(description.Equals(expectedResult[2].c_str())); + } +} + +TEST(TestServerTimingHeader, HeaderParsing) +{ + // Test cases below are copied from + // https://cs.chromium.org/chromium/src/third_party/WebKit/Source/platform/network/HTTPParsersTest.cpp + + testServerTimingHeader("", {}); + testServerTimingHeader("metric", {{"metric", "0", ""}}); + testServerTimingHeader("metric;dur", {{"metric", "0", ""}}); + testServerTimingHeader("metric;dur=123.4", {{"metric", "123.4", ""}}); + testServerTimingHeader("metric;dur=\"123.4\"", {{"metric", "123.4", ""}}); + + testServerTimingHeader("metric;desc", {{"metric", "0", ""}}); + testServerTimingHeader("metric;desc=description", + {{"metric", "0", "description"}}); + testServerTimingHeader("metric;desc=\"description\"", + {{"metric", "0", "description"}}); + + testServerTimingHeader("metric;dur;desc", {{"metric", "0", ""}}); + testServerTimingHeader("metric;dur=123.4;desc", {{"metric", "123.4", ""}}); + testServerTimingHeader("metric;dur;desc=description", + {{"metric", "0", "description"}}); + testServerTimingHeader("metric;dur=123.4;desc=description", + {{"metric", "123.4", "description"}}); + testServerTimingHeader("metric;desc;dur", {{"metric", "0", ""}}); + testServerTimingHeader("metric;desc;dur=123.4", {{"metric", "123.4", ""}}); + testServerTimingHeader("metric;desc=description;dur", + {{"metric", "0", "description"}}); + testServerTimingHeader("metric;desc=description;dur=123.4", + {{"metric", "123.4", "description"}}); + + // special chars in name + testServerTimingHeader("aB3!#$%&'*+-.^_`|~", + {{"aB3!#$%&'*+-.^_`|~", "0", ""}}); + + // delimiter chars in quoted description + testServerTimingHeader("metric;desc=\"descr;,=iption\";dur=123.4", + {{"metric", "123.4", "descr;,=iption"}}); + + // whitespace + testServerTimingHeader("metric ; ", {{"metric", "0", ""}}); + testServerTimingHeader("metric , ", {{"metric", "0", ""}}); + testServerTimingHeader("metric ; dur = 123.4 ; desc = description", + {{"metric", "123.4", "description"}}); + testServerTimingHeader("metric ; desc = description ; dur = 123.4", + {{"metric", "123.4", "description"}}); + + // multiple entries + testServerTimingHeader( + "metric1;dur=12.3;desc=description1,metric2;dur=45.6;" + "desc=description2,metric3;dur=78.9;desc=description3", + {{"metric1", "12.3", "description1"}, + {"metric2", "45.6", "description2"}, + {"metric3", "78.9", "description3"}}); + testServerTimingHeader("metric1,metric2 ,metric3, metric4 , metric5", + {{"metric1", "0", ""}, + {"metric2", "0", ""}, + {"metric3", "0", ""}, + {"metric4", "0", ""}, + {"metric5", "0", ""}}); + + // quoted-strings + // metric;desc=\ --> '' + testServerTimingHeader("metric;desc=\\", {{"metric", "0", ""}}); + // metric;desc=" --> '' + testServerTimingHeader("metric;desc=\"", {{"metric", "0", ""}}); + // metric;desc=\\ --> '' + testServerTimingHeader("metric;desc=\\\\", {{"metric", "0", ""}}); + // metric;desc=\" --> '' + testServerTimingHeader("metric;desc=\\\"", {{"metric", "0", ""}}); + // metric;desc="\ --> '' + testServerTimingHeader("metric;desc=\"\\", {{"metric", "0", ""}}); + // metric;desc="" --> '' + testServerTimingHeader("metric;desc=\"\"", {{"metric", "0", ""}}); + // metric;desc=\\\ --> '' + testServerTimingHeader(R"(metric;desc=\\\)", {{"metric", "0", ""}}); + // metric;desc=\\" --> '' + testServerTimingHeader(R"(metric;desc=\\")", {{"metric", "0", ""}}); + // metric;desc=\"\ --> '' + testServerTimingHeader(R"(metric;desc=\"\)", {{"metric", "0", ""}}); + // metric;desc=\"" --> '' + testServerTimingHeader(R"(metric;desc=\"")", {{"metric", "0", ""}}); + // metric;desc="\\ --> '' + testServerTimingHeader(R"(metric;desc="\\)", {{"metric", "0", ""}}); + // metric;desc="\" --> '' + testServerTimingHeader(R"(metric;desc="\")", {{"metric", "0", ""}}); + // metric;desc=""\ --> '' + testServerTimingHeader(R"(metric;desc=""\)", {{"metric", "0", ""}}); + // metric;desc=""" --> '' + testServerTimingHeader(R"(metric;desc=""")", {{"metric", "0", ""}}); + // metric;desc=\\\\ --> '' + testServerTimingHeader(R"(metric;desc=\\\\)", {{"metric", "0", ""}}); + // metric;desc=\\\" --> '' + testServerTimingHeader(R"(metric;desc=\\\")", {{"metric", "0", ""}}); + // metric;desc=\\"\ --> '' + testServerTimingHeader(R"(metric;desc=\\"\)", {{"metric", "0", ""}}); + // metric;desc=\\"" --> '' + testServerTimingHeader(R"(metric;desc=\\"")", {{"metric", "0", ""}}); + // metric;desc=\"\\ --> '' + testServerTimingHeader(R"(metric;desc=\"\\)", {{"metric", "0", ""}}); + // metric;desc=\"\" --> '' + testServerTimingHeader(R"(metric;desc=\"\")", {{"metric", "0", ""}}); + // metric;desc=\""\ --> '' + testServerTimingHeader(R"(metric;desc=\""\)", {{"metric", "0", ""}}); + // metric;desc=\""" --> '' + testServerTimingHeader(R"(metric;desc=\""")", {{"metric", "0", ""}}); + // metric;desc="\\\ --> '' + testServerTimingHeader(R"(metric;desc="\\\)", {{"metric", "0", ""}}); + // metric;desc="\\" --> '\' + testServerTimingHeader(R"(metric;desc="\\")", {{"metric", "0", "\\"}}); + // metric;desc="\"\ --> '' + testServerTimingHeader(R"(metric;desc="\"\)", {{"metric", "0", ""}}); + // metric;desc="\"" --> '"' + testServerTimingHeader(R"(metric;desc="\"")", {{"metric", "0", "\""}}); + // metric;desc=""\\ --> '' + testServerTimingHeader(R"(metric;desc=""\\)", {{"metric", "0", ""}}); + // metric;desc=""\" --> '' + testServerTimingHeader(R"(metric;desc=""\")", {{"metric", "0", ""}}); + // metric;desc="""\ --> '' + testServerTimingHeader(R"(metric;desc="""\)", {{"metric", "0", ""}}); + // metric;desc="""" --> '' + testServerTimingHeader(R"(metric;desc="""")", {{"metric", "0", ""}}); + + // duplicate entry names + testServerTimingHeader( + "metric;dur=12.3;desc=description1,metric;dur=45.6;" + "desc=description2", + {{"metric", "12.3", "description1"}, {"metric", "45.6", "description2"}}); + + // non-numeric durations + testServerTimingHeader("metric;dur=foo", {{"metric", "0", ""}}); + testServerTimingHeader("metric;dur=\"foo\"", {{"metric", "0", ""}}); + + // unrecognized param names + testServerTimingHeader( + "metric;foo=bar;desc=description;foo=bar;dur=123.4;foo=bar", + {{"metric", "123.4", "description"}}); + + // duplicate param names + testServerTimingHeader("metric;dur=123.4;dur=567.8", + {{"metric", "123.4", ""}}); + testServerTimingHeader("metric;desc=description1;desc=description2", + {{"metric", "0", "description1"}}); + testServerTimingHeader("metric;dur=foo;dur=567.8", {{"metric", "", ""}}); + + // unspecified param values + testServerTimingHeader("metric;dur;dur=123.4", {{"metric", "0", ""}}); + testServerTimingHeader("metric;desc;desc=description", {{"metric", "0", ""}}); + + // param name case + testServerTimingHeader("metric;DuR=123.4;DeSc=description", + {{"metric", "123.4", "description"}}); + + // nonsense + testServerTimingHeader("metric=foo;dur;dur=123.4,metric2", + {{"metric", "0", ""}, {"metric2", "0", ""}}); + testServerTimingHeader("metric\"foo;dur;dur=123.4,metric2", + {{"metric", "0", ""}}); + + // nonsense - return zero entries + testServerTimingHeader(" ", {}); + testServerTimingHeader("=", {}); + testServerTimingHeader("[", {}); + testServerTimingHeader("]", {}); + testServerTimingHeader(";", {}); + testServerTimingHeader(",", {}); + testServerTimingHeader("=;", {}); + testServerTimingHeader(";=", {}); + testServerTimingHeader("=,", {}); + testServerTimingHeader(",=", {}); + testServerTimingHeader(";,", {}); + testServerTimingHeader(",;", {}); + testServerTimingHeader("=;,", {}); + + // Invalid token + testServerTimingHeader("met=ric", {{"met", "0", ""}}); + testServerTimingHeader("met ric", {{"met", "0", ""}}); + testServerTimingHeader("met[ric", {{"met", "0", ""}}); + testServerTimingHeader("met]ric", {{"met", "0", ""}}); + testServerTimingHeader("metric;desc=desc=123, metric2", + {{"metric", "0", "desc"}, {"metric2", "0", ""}}); + testServerTimingHeader("met ric;desc=de sc , metric2", + {{"met", "0", "de"}, {"metric2", "0", ""}}); + + // test cases from https://w3c.github.io/server-timing/#examples + testServerTimingHeader( + " miss, ,db;dur=53, app;dur=47.2 ", + {{"miss", "0", ""}, {"db", "53", ""}, {"app", "47.2", ""}}); + testServerTimingHeader(" customView, dc;desc=atl ", + {{"customView", "0", ""}, {"dc", "0", "atl"}}); + testServerTimingHeader(" total;dur=123.4 ", {{"total", "123.4", ""}}); + + // test cases for comma in quoted string + testServerTimingHeader(R"( metric ; desc="descr\"\";,=iption";dur=123.4)", + {{"metric", "123.4", "descr\"\";,=iption"}}); + testServerTimingHeader( + " metric2;dur=\"123.4\";;desc=\",;\\\",;,\";;, metric ; desc = \" " + "\\\", ;\\\" \"; dur=123.4,", + {{"metric2", "123.4", ",;\",;,"}, {"metric", "123.4", " \", ;\" "}}); +} diff --git a/netwerk/test/gtest/TestSocketTransportService.cpp b/netwerk/test/gtest/TestSocketTransportService.cpp new file mode 100644 index 0000000000..89adad3740 --- /dev/null +++ b/netwerk/test/gtest/TestSocketTransportService.cpp @@ -0,0 +1,164 @@ +#include "gtest/gtest.h" + +#include "nsCOMPtr.h" +#include "nsISocketTransport.h" +#include "nsString.h" +#include "nsComponentManagerUtils.h" +#include "../../base/nsSocketTransportService2.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace net { + +TEST(TestSocketTransportService, PortRemappingPreferenceReading) +{ + nsCOMPtr<nsISocketTransportService> service = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + ASSERT_TRUE(service); + + auto* sts = gSocketTransportService; + ASSERT_TRUE(sts); + + NS_DispatchAndSpinEventLoopUntilComplete( + "test"_ns, sts, NS_NewRunnableFunction("test", [&]() { + auto CheckPortRemap = [&](uint16_t input, uint16_t output) -> bool { + sts->ApplyPortRemap(&input); + return input == output; + }; + + // Ill-formed prefs + ASSERT_FALSE(sts->UpdatePortRemapPreference(";"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference(" ;"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("; "_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("foo"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference(" foo"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference(" foo "_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("10=20;"_ns)); + + ASSERT_FALSE(sts->UpdatePortRemapPreference("1"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1="_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1,="_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-="_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-,="_ns)); + + ASSERT_FALSE(sts->UpdatePortRemapPreference("1=2,"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1=2-3"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-2,=3"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-2,3"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-2,3-4"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-2,3-4,"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-2,3-4="_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("10000000=10"_ns)); + + ASSERT_FALSE(sts->UpdatePortRemapPreference("1=2;3"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-4=2;3"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-4=2;3="_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-foo=2;3=15"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-4=foo;3=15"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-4=2;foo=15"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-4=2;3=foo"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1-4=2x3=15"_ns)); + ASSERT_FALSE(sts->UpdatePortRemapPreference("1+4=2;3=15"_ns)); + + // Well-formed prefs + ASSERT_TRUE(sts->UpdatePortRemapPreference("1=2"_ns)); + ASSERT_TRUE(CheckPortRemap(1, 2)); + ASSERT_TRUE(CheckPortRemap(2, 2)); + ASSERT_TRUE(CheckPortRemap(3, 3)); + + ASSERT_TRUE(sts->UpdatePortRemapPreference("10=20"_ns)); + ASSERT_TRUE(CheckPortRemap(1, 1)); + ASSERT_TRUE(CheckPortRemap(2, 2)); + ASSERT_TRUE(CheckPortRemap(3, 3)); + ASSERT_TRUE(CheckPortRemap(10, 20)); + + ASSERT_TRUE(sts->UpdatePortRemapPreference("100-200=1000"_ns)); + ASSERT_TRUE(CheckPortRemap(10, 10)); + ASSERT_TRUE(CheckPortRemap(99, 99)); + ASSERT_TRUE(CheckPortRemap(100, 1000)); + ASSERT_TRUE(CheckPortRemap(101, 1000)); + ASSERT_TRUE(CheckPortRemap(200, 1000)); + ASSERT_TRUE(CheckPortRemap(201, 201)); + + ASSERT_TRUE(sts->UpdatePortRemapPreference("100-200,500=1000"_ns)); + ASSERT_TRUE(CheckPortRemap(10, 10)); + ASSERT_TRUE(CheckPortRemap(99, 99)); + ASSERT_TRUE(CheckPortRemap(100, 1000)); + ASSERT_TRUE(CheckPortRemap(101, 1000)); + ASSERT_TRUE(CheckPortRemap(200, 1000)); + ASSERT_TRUE(CheckPortRemap(201, 201)); + ASSERT_TRUE(CheckPortRemap(499, 499)); + ASSERT_TRUE(CheckPortRemap(500, 1000)); + ASSERT_TRUE(CheckPortRemap(501, 501)); + + ASSERT_TRUE(sts->UpdatePortRemapPreference("1-3=10;5-8,12=20"_ns)); + ASSERT_TRUE(CheckPortRemap(1, 10)); + ASSERT_TRUE(CheckPortRemap(2, 10)); + ASSERT_TRUE(CheckPortRemap(3, 10)); + ASSERT_TRUE(CheckPortRemap(4, 4)); + ASSERT_TRUE(CheckPortRemap(5, 20)); + ASSERT_TRUE(CheckPortRemap(8, 20)); + ASSERT_TRUE(CheckPortRemap(11, 11)); + ASSERT_TRUE(CheckPortRemap(12, 20)); + + ASSERT_TRUE(sts->UpdatePortRemapPreference("80=8080;443=8080"_ns)); + ASSERT_TRUE(CheckPortRemap(80, 8080)); + ASSERT_TRUE(CheckPortRemap(443, 8080)); + + // Later rules rewrite earlier rules + ASSERT_TRUE(sts->UpdatePortRemapPreference("10=100;10=200"_ns)); + ASSERT_TRUE(CheckPortRemap(10, 200)); + + ASSERT_TRUE(sts->UpdatePortRemapPreference("10-20=100;10-20=200"_ns)); + ASSERT_TRUE(CheckPortRemap(10, 200)); + ASSERT_TRUE(CheckPortRemap(20, 200)); + + ASSERT_TRUE(sts->UpdatePortRemapPreference("10-20=100;15=200"_ns)); + ASSERT_TRUE(CheckPortRemap(10, 100)); + ASSERT_TRUE(CheckPortRemap(15, 200)); + ASSERT_TRUE(CheckPortRemap(20, 100)); + + ASSERT_TRUE(sts->UpdatePortRemapPreference( + " 100 - 200 = 1000 ; 150 = 2000 "_ns)); + ASSERT_TRUE(CheckPortRemap(100, 1000)); + ASSERT_TRUE(CheckPortRemap(150, 2000)); + ASSERT_TRUE(CheckPortRemap(200, 1000)); + + // Turn off any mapping + ASSERT_TRUE(sts->UpdatePortRemapPreference(""_ns)); + for (uint32_t port = 0; port < 65536; ++port) { + ASSERT_TRUE(CheckPortRemap((uint16_t)port, (uint16_t)port)); + } + })); +} + +TEST(TestSocketTransportService, StatusValues) +{ + static_assert(static_cast<nsresult>(nsISocketTransport::STATUS_RESOLVING) == + NS_NET_STATUS_RESOLVING_HOST); + static_assert(static_cast<nsresult>(nsISocketTransport::STATUS_RESOLVED) == + NS_NET_STATUS_RESOLVED_HOST); + static_assert( + static_cast<nsresult>(nsISocketTransport::STATUS_CONNECTING_TO) == + NS_NET_STATUS_CONNECTING_TO); + static_assert( + static_cast<nsresult>(nsISocketTransport::STATUS_CONNECTED_TO) == + NS_NET_STATUS_CONNECTED_TO); + static_assert(static_cast<nsresult>(nsISocketTransport::STATUS_SENDING_TO) == + NS_NET_STATUS_SENDING_TO); + static_assert(static_cast<nsresult>(nsISocketTransport::STATUS_WAITING_FOR) == + NS_NET_STATUS_WAITING_FOR); + static_assert( + static_cast<nsresult>(nsISocketTransport::STATUS_RECEIVING_FROM) == + NS_NET_STATUS_RECEIVING_FROM); + static_assert(static_cast<nsresult>( + nsISocketTransport::STATUS_TLS_HANDSHAKE_STARTING) == + NS_NET_STATUS_TLS_HANDSHAKE_STARTING); + static_assert( + static_cast<nsresult>(nsISocketTransport::STATUS_TLS_HANDSHAKE_ENDED) == + NS_NET_STATUS_TLS_HANDSHAKE_ENDED); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/test/gtest/TestStandardURL.cpp b/netwerk/test/gtest/TestStandardURL.cpp new file mode 100644 index 0000000000..877539c607 --- /dev/null +++ b/netwerk/test/gtest/TestStandardURL.cpp @@ -0,0 +1,418 @@ +#include "gtest/gtest.h" +#include "gtest/MozGTestBench.h" // For MOZ_GTEST_BENCH + +#include "nsCOMPtr.h" +#include "nsNetCID.h" +#include "nsIURL.h" +#include "nsIStandardURL.h" +#include "nsString.h" +#include "nsPrintfCString.h" +#include "nsComponentManagerUtils.h" +#include "nsIURIMutator.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/Unused.h" +#include "nsSerializationHelper.h" +#include "mozilla/Base64.h" +#include "nsEscape.h" + +using namespace mozilla; + +// In nsStandardURL.cpp +extern nsresult Test_NormalizeIPv4(const nsACString& host, nsCString& result); + +TEST(TestStandardURL, Simple) +{ + nsCOMPtr<nsIURI> url; + ASSERT_EQ(NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec("http://example.com"_ns) + .Finalize(url), + NS_OK); + ASSERT_TRUE(url); + + ASSERT_EQ(NS_MutateURI(url).SetSpec("http://example.com"_ns).Finalize(url), + NS_OK); + + nsAutoCString out; + + ASSERT_EQ(url->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "http://example.com/"_ns); + + ASSERT_EQ(url->Resolve("foo.html?q=45"_ns, out), NS_OK); + ASSERT_TRUE(out == "http://example.com/foo.html?q=45"_ns); + + ASSERT_EQ(NS_MutateURI(url).SetScheme("foo"_ns).Finalize(url), NS_OK); + + ASSERT_EQ(url->GetScheme(out), NS_OK); + ASSERT_TRUE(out == "foo"_ns); + + ASSERT_EQ(url->GetHost(out), NS_OK); + ASSERT_TRUE(out == "example.com"_ns); + ASSERT_EQ(NS_MutateURI(url).SetHost("www.yahoo.com"_ns).Finalize(url), NS_OK); + ASSERT_EQ(url->GetHost(out), NS_OK); + ASSERT_TRUE(out == "www.yahoo.com"_ns); + + ASSERT_EQ(NS_MutateURI(url) + .SetPathQueryRef(nsLiteralCString( + "/some-path/one-the-net/about.html?with-a-query#for-you")) + .Finalize(url), + NS_OK); + ASSERT_EQ(url->GetPathQueryRef(out), NS_OK); + ASSERT_TRUE(out == + nsLiteralCString( + "/some-path/one-the-net/about.html?with-a-query#for-you")); + + ASSERT_EQ(NS_MutateURI(url) + .SetQuery(nsLiteralCString( + "a=b&d=c&what-ever-you-want-to-be-called=45")) + .Finalize(url), + NS_OK); + ASSERT_EQ(url->GetQuery(out), NS_OK); + ASSERT_TRUE(out == "a=b&d=c&what-ever-you-want-to-be-called=45"_ns); + + ASSERT_EQ(NS_MutateURI(url).SetRef("#some-book-mark"_ns).Finalize(url), + NS_OK); + ASSERT_EQ(url->GetRef(out), NS_OK); + ASSERT_TRUE(out == "some-book-mark"_ns); +} + +TEST(TestStandardURL, NormalizeGood) +{ + nsCString result; + const char* manual[] = {"0.0.0.0", + "0.0.0.0", + "0", + "0.0.0.0", + "000", + "0.0.0.0", + "0x00", + "0.0.0.0", + "10.20.100.200", + "10.20.100.200", + "255.255.255.255", + "255.255.255.255", + "0XFF.0xFF.0xff.0xFf", + "255.255.255.255", + "0x000ff.0X00FF.0x0ff.0xff", + "255.255.255.255", + "0x000fA.0X00FB.0x0fC.0xfD", + "250.251.252.253", + "0x000fE.0X00FF.0x0fC.0xfD", + "254.255.252.253", + "0x000fa.0x00fb.0x0fc.0xfd", + "250.251.252.253", + "0x000fe.0x00ff.0x0fc.0xfd", + "254.255.252.253", + "0377.0377.0377.0377", + "255.255.255.255", + "0000377.000377.00377.0377", + "255.255.255.255", + "65535", + "0.0.255.255", + "0xfFFf", + "0.0.255.255", + "0x00000ffff", + "0.0.255.255", + "0177777", + "0.0.255.255", + "000177777", + "0.0.255.255", + "0.13.65535", + "0.13.255.255", + "0.22.0xffff", + "0.22.255.255", + "0.123.0177777", + "0.123.255.255", + "65536", + "0.1.0.0", + "0200000", + "0.1.0.0", + "0x10000", + "0.1.0.0"}; + for (uint32_t i = 0; i < sizeof(manual) / sizeof(manual[0]); i += 2) { + nsCString encHost(manual[i + 0]); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHost, result)); + ASSERT_TRUE(result.Equals(manual[i + 1])); + } + + // Make sure we're getting the numbers correctly interpreted: + for (int i = 0; i < 256; i++) { + nsCString encHost = nsPrintfCString("0x%x", i); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHost, result)); + ASSERT_TRUE(result.Equals(nsPrintfCString("0.0.0.%d", i))); + + encHost = nsPrintfCString("0%o", i); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHost, result)); + ASSERT_TRUE(result.Equals(nsPrintfCString("0.0.0.%d", i))); + } + + // Some random numbers in the range, mixing hex, decimal, octal + for (int i = 0; i < 8; i++) { + int val[4] = {i * 11 + 13, i * 18 + 22, i * 4 + 28, i * 15 + 2}; + + nsCString encHost = + nsPrintfCString("%d.%d.%d.%d", val[0], val[1], val[2], val[3]); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHost, result)); + ASSERT_TRUE(result.Equals(encHost)); + + nsCString encHostM = + nsPrintfCString("0x%x.0x%x.0x%x.0x%x", val[0], val[1], val[2], val[3]); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHostM, result)); + ASSERT_TRUE(result.Equals(encHost)); + + encHostM = + nsPrintfCString("0%o.0%o.0%o.0%o", val[0], val[1], val[2], val[3]); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHostM, result)); + ASSERT_TRUE(result.Equals(encHost)); + + encHostM = + nsPrintfCString("0x%x.%d.0%o.%d", val[0], val[1], val[2], val[3]); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHostM, result)); + ASSERT_TRUE(result.Equals(encHost)); + + encHostM = + nsPrintfCString("%d.0%o.0%o.0x%x", val[0], val[1], val[2], val[3]); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHostM, result)); + ASSERT_TRUE(result.Equals(encHost)); + + encHostM = + nsPrintfCString("0%o.0%o.0x%x.0x%x", val[0], val[1], val[2], val[3]); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHostM, result)); + ASSERT_TRUE(result.Equals(encHost)); + } +} + +TEST(TestStandardURL, NormalizeBad) +{ + nsAutoCString result; + const char* manual[] = { + "x22.232.12.32", "122..12.32", "122.12.32.12.32", "122.12.32..", + "122.12.xx.22", "122.12.0xx.22", "0xx.12.01.22", "0x.12.01.22", + "12.12.02x.22", "1q.12.2.22", "122.01f.02.22", "12a.01.02.22", + "12.01.02.20x1", "10x2.01.02.20", "0xx.01.02.20", "10.x.02.20", + "10.00x2.02.20", "10.13.02x2.20", "10.x13.02.20", "10.0x134def.02.20", + "\0.2.2.2", "256.2.2.2", "2.256.2.2", "2.2.256.2", + "2.2.2.256", "2.2.-2.3", "+2.2.2.3", "13.0x2x2.2.3", + "0x2x2.13.2.3"}; + + for (auto& i : manual) { + nsCString encHost(i); + ASSERT_EQ(NS_ERROR_FAILURE, Test_NormalizeIPv4(encHost, result)); + } +} + +TEST(TestStandardURL, From_test_standardurldotjs) +{ + // These are test (success and failure) cases from test_standardurl.js + nsAutoCString result; + + const char* localIPv4s[] = { + "127.0.0.1", + "127.0.1", + "127.1", + "2130706433", + "0177.00.00.01", + "0177.00.01", + "0177.01", + "00000000000000000000000000177.0000000.0000000.0001", + "000000177.0000001", + "017700000001", + "0x7f.0x00.0x00.0x01", + "0x7f.0x01", + "0x7f000001", + "0x007f.0x0000.0x0000.0x0001", + "000177.0.00000.0x0001", + "127.0.0.1.", + + "0X7F.0X00.0X00.0X01", + "0X7F.0X01", + "0X7F000001", + "0X007F.0X0000.0X0000.0X0001", + "000177.0.00000.0X0001"}; + for (auto& localIPv4 : localIPv4s) { + nsCString encHost(localIPv4); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHost, result)); + ASSERT_TRUE(result.EqualsLiteral("127.0.0.1")); + } + + const char* nonIPv4s[] = {"0xfffffffff", "0x100000000", + "4294967296", "1.2.0x10000", + "1.0x1000000", "256.0.0.1", + "1.256.1", "-1.0.0.0", + "1.2.3.4.5", "010000000000000000", + "2+3", "0.0.0.-1", + "1.2.3.4..", "1..2", + ".1.2.3.4", ".127"}; + for (auto& nonIPv4 : nonIPv4s) { + nsCString encHost(nonIPv4); + ASSERT_EQ(NS_ERROR_FAILURE, Test_NormalizeIPv4(encHost, result)); + } + + const char* oneOrNoDotsIPv4s[] = {"127", "127."}; + for (auto& localIPv4 : oneOrNoDotsIPv4s) { + nsCString encHost(localIPv4); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHost, result)); + ASSERT_TRUE(result.EqualsLiteral("0.0.0.127")); + } +} + +#define TEST_COUNT 10000 + +MOZ_GTEST_BENCH(TestStandardURL, DISABLED_Perf, [] { + nsCOMPtr<nsIURI> url; + ASSERT_EQ(NS_OK, NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec("http://example.com"_ns) + .Finalize(url)); + + nsAutoCString out; + for (int i = TEST_COUNT; i; --i) { + ASSERT_EQ(NS_MutateURI(url).SetSpec("http://example.com"_ns).Finalize(url), + NS_OK); + ASSERT_EQ(url->GetSpec(out), NS_OK); + url->Resolve("foo.html?q=45"_ns, out); + mozilla::Unused << NS_MutateURI(url).SetScheme("foo"_ns).Finalize(url); + url->GetScheme(out); + mozilla::Unused + << NS_MutateURI(url).SetHost("www.yahoo.com"_ns).Finalize(url); + url->GetHost(out); + mozilla::Unused + << NS_MutateURI(url) + .SetPathQueryRef(nsLiteralCString( + "/some-path/one-the-net/about.html?with-a-query#for-you")) + .Finalize(url); + url->GetPathQueryRef(out); + mozilla::Unused << NS_MutateURI(url) + .SetQuery(nsLiteralCString( + "a=b&d=c&what-ever-you-want-to-be-called=45")) + .Finalize(url); + url->GetQuery(out); + mozilla::Unused + << NS_MutateURI(url).SetRef("#some-book-mark"_ns).Finalize(url); + url->GetRef(out); + } +}); + +// Note the five calls in the loop, so divide by 100k +MOZ_GTEST_BENCH(TestStandardURL, DISABLED_NormalizePerf, [] { + nsAutoCString result; + for (int i = 0; i < 20000; i++) { + nsAutoCString encHost("123.232.12.32"); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHost, result)); + nsAutoCString encHost2("83.62.12.92"); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHost2, result)); + nsAutoCString encHost3("8.7.6.5"); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHost3, result)); + nsAutoCString encHost4("111.159.123.220"); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHost4, result)); + nsAutoCString encHost5("1.160.204.200"); + ASSERT_EQ(NS_OK, Test_NormalizeIPv4(encHost5, result)); + } +}); + +// Bug 1394785 - ignore unstable test on OSX +#ifndef XP_MACOSX +// Note the five calls in the loop, so divide by 100k +MOZ_GTEST_BENCH(TestStandardURL, DISABLED_NormalizePerfFails, [] { + nsAutoCString result; + for (int i = 0; i < 20000; i++) { + nsAutoCString encHost("123.292.12.32"); + ASSERT_EQ(NS_ERROR_FAILURE, Test_NormalizeIPv4(encHost, result)); + nsAutoCString encHost2("83.62.12.0x13292"); + ASSERT_EQ(NS_ERROR_FAILURE, Test_NormalizeIPv4(encHost2, result)); + nsAutoCString encHost3("8.7.6.0xhello"); + ASSERT_EQ(NS_ERROR_FAILURE, Test_NormalizeIPv4(encHost3, result)); + nsAutoCString encHost4("111.159.notonmywatch.220"); + ASSERT_EQ(NS_ERROR_FAILURE, Test_NormalizeIPv4(encHost4, result)); + nsAutoCString encHost5("1.160.204.20f"); + ASSERT_EQ(NS_ERROR_FAILURE, Test_NormalizeIPv4(encHost5, result)); + } +}); +#endif + +TEST(TestStandardURL, Mutator) +{ + nsAutoCString out; + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec("http://example.com"_ns) + .Finalize(uri); + ASSERT_EQ(rv, NS_OK); + + ASSERT_EQ(uri->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "http://example.com/"_ns); + + rv = NS_MutateURI(uri) + .SetScheme("ftp"_ns) + .SetHost("mozilla.org"_ns) + .SetPathQueryRef("/path?query#ref"_ns) + .Finalize(uri); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(uri->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "ftp://mozilla.org/path?query#ref"_ns); + + nsCOMPtr<nsIURL> url; + rv = NS_MutateURI(uri).SetScheme("https"_ns).Finalize(url); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(url->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "https://mozilla.org/path?query#ref"_ns); +} + +TEST(TestStandardURL, Deserialize_Bug1392739) +{ + mozilla::ipc::StandardURLParams standard_params; + standard_params.urlType() = nsIStandardURL::URLTYPE_STANDARD; + standard_params.spec().Truncate(); + standard_params.host() = mozilla::ipc::StandardURLSegment(4294967295, 1); + + mozilla::ipc::URIParams params(standard_params); + + nsCOMPtr<nsIURIMutator> mutator = + do_CreateInstance(NS_STANDARDURLMUTATOR_CID); + ASSERT_EQ(mutator->Deserialize(params), NS_ERROR_FAILURE); +} + +TEST(TestStandardURL, CorruptSerialization) +{ + auto spec = "http://user:pass@example.com/path/to/file.ext?query#hash"_ns; + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec(spec) + .Finalize(uri); + ASSERT_EQ(rv, NS_OK); + + nsAutoCString serialization; + nsCOMPtr<nsISerializable> serializable = do_QueryInterface(uri); + ASSERT_TRUE(serializable); + + // Check that the URL is normally serializable. + ASSERT_EQ(NS_OK, NS_SerializeToString(serializable, serialization)); + nsCOMPtr<nsISupports> deserializedObject; + ASSERT_EQ(NS_OK, NS_DeserializeObject(serialization, + getter_AddRefs(deserializedObject))); + + nsAutoCString canonicalBin; + Unused << Base64Decode(serialization, canonicalBin); + +// The spec serialization begins at byte 49 +// If the implementation of nsStandardURL::Write changes, this test will need +// to be adjusted. +#define SPEC_OFFSET 49 + + ASSERT_EQ(Substring(canonicalBin, SPEC_OFFSET, 7), "http://"_ns); + + nsAutoCString corruptedBin = canonicalBin; + // change mScheme.mPos + corruptedBin.BeginWriting()[SPEC_OFFSET + spec.Length()] = 1; + Unused << Base64Encode(corruptedBin, serialization); + ASSERT_EQ( + NS_ERROR_MALFORMED_URI, + NS_DeserializeObject(serialization, getter_AddRefs(deserializedObject))); + + corruptedBin = canonicalBin; + // change mScheme.mLen + corruptedBin.BeginWriting()[SPEC_OFFSET + spec.Length() + 4] = 127; + Unused << Base64Encode(corruptedBin, serialization); + ASSERT_EQ( + NS_ERROR_MALFORMED_URI, + NS_DeserializeObject(serialization, getter_AddRefs(deserializedObject))); +} diff --git a/netwerk/test/gtest/TestUDPSocket.cpp b/netwerk/test/gtest/TestUDPSocket.cpp new file mode 100644 index 0000000000..e4f4985549 --- /dev/null +++ b/netwerk/test/gtest/TestUDPSocket.cpp @@ -0,0 +1,405 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TestCommon.h" +#include "gtest/gtest.h" +#include "nsIUDPSocket.h" +#include "nsISocketTransport.h" +#include "nsIOutputStream.h" +#include "nsINetAddr.h" +#include "nsITimer.h" +#include "nsContentUtils.h" +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/net/DNS.h" +#include "prerror.h" +#include "nsComponentManagerUtils.h" + +#define REQUEST 0x68656c6f +#define RESPONSE 0x6f6c6568 +#define MULTICAST_TIMEOUT 2000 + +enum TestPhase { TEST_OUTPUT_STREAM, TEST_SEND_API, TEST_MULTICAST, TEST_NONE }; + +static TestPhase phase = TEST_NONE; + +static bool CheckMessageContent(nsIUDPMessage* aMessage, + uint32_t aExpectedContent) { + nsCString data; + aMessage->GetData(data); + + const char* buffer = data.get(); + uint32_t len = data.Length(); + + FallibleTArray<uint8_t>& rawData = aMessage->GetDataAsTArray(); + uint32_t rawLen = rawData.Length(); + + if (len != rawLen) { + ADD_FAILURE() << "Raw data length " << rawLen + << " does not match String data length " << len; + return false; + } + + for (uint32_t i = 0; i < len; i++) { + if (buffer[i] != rawData[i]) { + ADD_FAILURE(); + return false; + } + } + + uint32_t input = 0; + for (uint32_t i = 0; i < len; i++) { + input += buffer[i] << (8 * i); + } + + if (len != sizeof(uint32_t)) { + ADD_FAILURE() << "Message length mismatch, expected " << sizeof(uint32_t) + << " got " << len; + return false; + } + if (input != aExpectedContent) { + ADD_FAILURE() << "Message content mismatch, expected 0x" << std::hex + << aExpectedContent << " got 0x" << input; + return false; + } + + return true; +} + +/* + * UDPClientListener: listens for incomming UDP packets + */ +class UDPClientListener : public nsIUDPSocketListener { + protected: + virtual ~UDPClientListener(); + + public: + explicit UDPClientListener(WaitForCondition* waiter) : mWaiter(waiter) {} + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIUDPSOCKETLISTENER + nsresult mResult = NS_ERROR_FAILURE; + RefPtr<WaitForCondition> mWaiter; +}; + +NS_IMPL_ISUPPORTS(UDPClientListener, nsIUDPSocketListener) + +UDPClientListener::~UDPClientListener() = default; + +NS_IMETHODIMP +UDPClientListener::OnPacketReceived(nsIUDPSocket* socket, + nsIUDPMessage* message) { + mResult = NS_OK; + + uint16_t port; + nsCString ip; + nsCOMPtr<nsINetAddr> fromAddr; + message->GetFromAddr(getter_AddRefs(fromAddr)); + fromAddr->GetPort(&port); + fromAddr->GetAddress(ip); + + if (TEST_SEND_API == phase && CheckMessageContent(message, REQUEST)) { + uint32_t count; + nsTArray<uint8_t> data; + const uint32_t dataBuffer = RESPONSE; + data.AppendElements((const uint8_t*)&dataBuffer, sizeof(uint32_t)); + mResult = socket->SendWithAddr(fromAddr, data, &count); + if (mResult == NS_OK && count == sizeof(uint32_t)) { + SUCCEED(); + } else { + ADD_FAILURE(); + } + return NS_OK; + } + if (TEST_OUTPUT_STREAM != phase || !CheckMessageContent(message, RESPONSE)) { + mResult = NS_ERROR_FAILURE; + } + + // Notify thread + mWaiter->Notify(); + return NS_OK; +} + +NS_IMETHODIMP +UDPClientListener::OnStopListening(nsIUDPSocket*, nsresult) { + mWaiter->Notify(); + return NS_OK; +} + +/* + * UDPServerListener: listens for incomming UDP packets + */ +class UDPServerListener : public nsIUDPSocketListener { + protected: + virtual ~UDPServerListener(); + + public: + explicit UDPServerListener(WaitForCondition* waiter) : mWaiter(waiter) {} + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIUDPSOCKETLISTENER + + nsresult mResult = NS_ERROR_FAILURE; + RefPtr<WaitForCondition> mWaiter; +}; + +NS_IMPL_ISUPPORTS(UDPServerListener, nsIUDPSocketListener) + +UDPServerListener::~UDPServerListener() = default; + +NS_IMETHODIMP +UDPServerListener::OnPacketReceived(nsIUDPSocket* socket, + nsIUDPMessage* message) { + mResult = NS_OK; + + uint16_t port; + nsCString ip; + nsCOMPtr<nsINetAddr> fromAddr; + message->GetFromAddr(getter_AddRefs(fromAddr)); + fromAddr->GetPort(&port); + fromAddr->GetAddress(ip); + SUCCEED(); + + if (TEST_OUTPUT_STREAM == phase && CheckMessageContent(message, REQUEST)) { + nsCOMPtr<nsIOutputStream> outstream; + message->GetOutputStream(getter_AddRefs(outstream)); + + uint32_t count; + const uint32_t data = RESPONSE; + mResult = outstream->Write((const char*)&data, sizeof(uint32_t), &count); + + if (mResult == NS_OK && count == sizeof(uint32_t)) { + SUCCEED(); + } else { + ADD_FAILURE(); + } + return NS_OK; + } + if (TEST_MULTICAST == phase && CheckMessageContent(message, REQUEST)) { + mResult = NS_OK; + } else if (TEST_SEND_API != phase || + !CheckMessageContent(message, RESPONSE)) { + mResult = NS_ERROR_FAILURE; + } + + // Notify thread + mWaiter->Notify(); + return NS_OK; +} + +NS_IMETHODIMP +UDPServerListener::OnStopListening(nsIUDPSocket*, nsresult) { + mWaiter->Notify(); + return NS_OK; +} + +/** + * Multicast timer callback: detects delivery failure + */ +class MulticastTimerCallback : public nsITimerCallback, public nsINamed { + protected: + virtual ~MulticastTimerCallback(); + + public: + explicit MulticastTimerCallback(WaitForCondition* waiter) + : mResult(NS_ERROR_NOT_INITIALIZED), mWaiter(waiter) {} + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + nsresult mResult; + RefPtr<WaitForCondition> mWaiter; +}; + +NS_IMPL_ISUPPORTS(MulticastTimerCallback, nsITimerCallback, nsINamed) + +MulticastTimerCallback::~MulticastTimerCallback() = default; + +NS_IMETHODIMP +MulticastTimerCallback::Notify(nsITimer* timer) { + if (TEST_MULTICAST != phase) { + return NS_OK; + } + // Multicast ping failed + printf("Multicast ping timeout expired\n"); + mResult = NS_ERROR_FAILURE; + mWaiter->Notify(); + return NS_OK; +} + +NS_IMETHODIMP +MulticastTimerCallback::GetName(nsACString& aName) { + aName.AssignLiteral("MulticastTimerCallback"); + return NS_OK; +} + +/**** Main ****/ + +TEST(TestUDPSocket, TestUDPSocketMain) +{ + nsresult rv; + + // Create UDPSocket + nsCOMPtr<nsIUDPSocket> server, client; + server = do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv); + ASSERT_NS_SUCCEEDED(rv); + + client = do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv); + ASSERT_NS_SUCCEEDED(rv); + + RefPtr<WaitForCondition> waiter = new WaitForCondition(); + + // Create UDPServerListener to process UDP packets + RefPtr<UDPServerListener> serverListener = new UDPServerListener(waiter); + + nsCOMPtr<nsIPrincipal> systemPrincipal = nsContentUtils::GetSystemPrincipal(); + + // Bind server socket to 0.0.0.0 + rv = server->Init(0, false, systemPrincipal, true, 0); + ASSERT_NS_SUCCEEDED(rv); + int32_t serverPort; + server->GetPort(&serverPort); + server->AsyncListen(serverListener); + + // Bind clinet on arbitrary port + RefPtr<UDPClientListener> clientListener = new UDPClientListener(waiter); + client->Init(0, false, systemPrincipal, true, 0); + client->AsyncListen(clientListener); + + // Write data to server + uint32_t count; + nsTArray<uint8_t> data; + const uint32_t dataBuffer = REQUEST; + data.AppendElements((const uint8_t*)&dataBuffer, sizeof(uint32_t)); + + phase = TEST_OUTPUT_STREAM; + rv = client->Send("127.0.0.1"_ns, serverPort, data, &count); + ASSERT_NS_SUCCEEDED(rv); + EXPECT_EQ(count, sizeof(uint32_t)); + + // Wait for server + waiter->Wait(1); + ASSERT_NS_SUCCEEDED(serverListener->mResult); + + // Read response from server + ASSERT_NS_SUCCEEDED(clientListener->mResult); + + mozilla::net::NetAddr clientAddr; + rv = client->GetAddress(&clientAddr); + ASSERT_NS_SUCCEEDED(rv); + // The client address is 0.0.0.0, but Windows won't receive packets there, so + // use 127.0.0.1 explicitly + clientAddr.inet.ip = PR_htonl(127 << 24 | 1); + + phase = TEST_SEND_API; + rv = server->SendWithAddress(&clientAddr, data, &count); + ASSERT_NS_SUCCEEDED(rv); + EXPECT_EQ(count, sizeof(uint32_t)); + + // Wait for server + waiter->Wait(1); + ASSERT_NS_SUCCEEDED(serverListener->mResult); + + // Read response from server + ASSERT_NS_SUCCEEDED(clientListener->mResult); + + // Setup timer to detect multicast failure + nsCOMPtr<nsITimer> timer = NS_NewTimer(); + ASSERT_TRUE(timer); + RefPtr<MulticastTimerCallback> timerCb = new MulticastTimerCallback(waiter); + + // Join multicast group + printf("Joining multicast group\n"); + phase = TEST_MULTICAST; + mozilla::net::NetAddr multicastAddr; + multicastAddr.inet.family = AF_INET; + multicastAddr.inet.ip = PR_htonl(224 << 24 | 255); + multicastAddr.inet.port = PR_htons(serverPort); + rv = server->JoinMulticastAddr(multicastAddr, nullptr); + ASSERT_NS_SUCCEEDED(rv); + + // Send multicast ping + timerCb->mResult = NS_OK; + timer->InitWithCallback(timerCb, MULTICAST_TIMEOUT, nsITimer::TYPE_ONE_SHOT); + rv = client->SendWithAddress(&multicastAddr, data, &count); + ASSERT_NS_SUCCEEDED(rv); + EXPECT_EQ(count, sizeof(uint32_t)); + + // Wait for server to receive successfully + waiter->Wait(1); + ASSERT_NS_SUCCEEDED(serverListener->mResult); + ASSERT_NS_SUCCEEDED(timerCb->mResult); + timer->Cancel(); + + // Disable multicast loopback + printf("Disable multicast loopback\n"); + client->SetMulticastLoopback(false); + server->SetMulticastLoopback(false); + + // Send multicast ping + timerCb->mResult = NS_OK; + timer->InitWithCallback(timerCb, MULTICAST_TIMEOUT, nsITimer::TYPE_ONE_SHOT); + rv = client->SendWithAddress(&multicastAddr, data, &count); + ASSERT_NS_SUCCEEDED(rv); + EXPECT_EQ(count, sizeof(uint32_t)); + + // Wait for server to fail to receive + waiter->Wait(1); + ASSERT_FALSE(NS_SUCCEEDED(timerCb->mResult)); + timer->Cancel(); + + // Reset state + client->SetMulticastLoopback(true); + server->SetMulticastLoopback(true); + + // Change multicast interface + mozilla::net::NetAddr loopbackAddr; + loopbackAddr.inet.family = AF_INET; + loopbackAddr.inet.ip = PR_htonl(INADDR_LOOPBACK); + client->SetMulticastInterfaceAddr(loopbackAddr); + + // Send multicast ping + timerCb->mResult = NS_OK; + timer->InitWithCallback(timerCb, MULTICAST_TIMEOUT, nsITimer::TYPE_ONE_SHOT); + rv = client->SendWithAddress(&multicastAddr, data, &count); + ASSERT_NS_SUCCEEDED(rv); + EXPECT_EQ(count, sizeof(uint32_t)); + + // Wait for server to fail to receive + waiter->Wait(1); + ASSERT_FALSE(NS_SUCCEEDED(timerCb->mResult)); + timer->Cancel(); + + // Reset state + mozilla::net::NetAddr anyAddr; + anyAddr.inet.family = AF_INET; + anyAddr.inet.ip = PR_htonl(INADDR_ANY); + client->SetMulticastInterfaceAddr(anyAddr); + + // Leave multicast group + rv = server->LeaveMulticastAddr(multicastAddr, nullptr); + ASSERT_NS_SUCCEEDED(rv); + + // Send multicast ping + timerCb->mResult = NS_OK; + timer->InitWithCallback(timerCb, MULTICAST_TIMEOUT, nsITimer::TYPE_ONE_SHOT); + rv = client->SendWithAddress(&multicastAddr, data, &count); + ASSERT_NS_SUCCEEDED(rv); + EXPECT_EQ(count, sizeof(uint32_t)); + + // Wait for server to fail to receive + waiter->Wait(1); + ASSERT_FALSE(NS_SUCCEEDED(timerCb->mResult)); + timer->Cancel(); + + goto close; // suppress warning about unused label + +close: + // Close server + server->Close(); + client->Close(); + + // Wait for client and server to see closing + waiter->Wait(2); +} diff --git a/netwerk/test/gtest/TestURIMutator.cpp b/netwerk/test/gtest/TestURIMutator.cpp new file mode 100644 index 0000000000..255ed640eb --- /dev/null +++ b/netwerk/test/gtest/TestURIMutator.cpp @@ -0,0 +1,163 @@ +#include "gtest/gtest.h" +#include "nsCOMPtr.h" +#include "nsNetCID.h" +#include "nsIURIMutator.h" +#include "nsIURL.h" +#include "nsThreadPool.h" +#include "nsNetUtil.h" + +TEST(TestURIMutator, Mutator) +{ + nsAutoCString out; + + // This test instantiates a new nsStandardURL::Mutator (via contractID) + // and uses it to create a new URI. + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec("http://example.com"_ns) + .Finalize(uri); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(uri->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "http://example.com/"_ns); + + // This test verifies that we can use NS_MutateURI to change a URI + rv = NS_MutateURI(uri) + .SetScheme("ftp"_ns) + .SetHost("mozilla.org"_ns) + .SetPathQueryRef("/path?query#ref"_ns) + .Finalize(uri); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(uri->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "ftp://mozilla.org/path?query#ref"_ns); + + // This test verifies that we can pass nsIURL to Finalize, and + nsCOMPtr<nsIURL> url; + rv = NS_MutateURI(uri).SetScheme("https"_ns).Finalize(url); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(url->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "https://mozilla.org/path?query#ref"_ns); + + // This test verifies that we can pass nsIURL** to Finalize. + // We need to use the explicit template because it's actually passing + // getter_AddRefs + nsCOMPtr<nsIURL> url2; + rv = NS_MutateURI(url) + .SetRef("newref"_ns) + .Finalize<nsIURL>(getter_AddRefs(url2)); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(url2->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "https://mozilla.org/path?query#newref"_ns); + + // This test verifies that we can pass nsIURI** to Finalize. + // No need to be explicit. + auto functionSetRef = [](nsIURI* aURI, nsIURI** aResult) -> nsresult { + return NS_MutateURI(aURI).SetRef("originalRef"_ns).Finalize(aResult); + }; + + nsCOMPtr<nsIURI> newURI; + rv = functionSetRef(url2, getter_AddRefs(newURI)); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(newURI->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "https://mozilla.org/path?query#originalRef"_ns); + + // This test verifies that we can pass nsIURI** to Finalize. + nsCOMPtr<nsIURI> uri2; + rv = + NS_MutateURI(url2).SetQuery("newquery"_ns).Finalize(getter_AddRefs(uri2)); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(uri2->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "https://mozilla.org/path?newquery#newref"_ns); + + // This test verifies that we can pass nsIURI** to Finalize. + // No need to be explicit. + auto functionSetQuery = [](nsIURI* aURI, nsIURL** aResult) -> nsresult { + return NS_MutateURI(aURI).SetQuery("originalQuery"_ns).Finalize(aResult); + }; + + nsCOMPtr<nsIURL> newURL; + rv = functionSetQuery(uri2, getter_AddRefs(newURL)); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(newURL->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "https://mozilla.org/path?originalQuery#newref"_ns); + + // Check that calling Finalize twice will fail. + NS_MutateURI mutator(newURL); + rv = mutator.SetQuery(""_ns).Finalize(uri2); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(uri2->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "https://mozilla.org/path#newref"_ns); + nsCOMPtr<nsIURI> uri3; + rv = mutator.Finalize(uri3); + ASSERT_EQ(rv, NS_ERROR_NOT_AVAILABLE); + ASSERT_TRUE(uri3 == nullptr); + + // Make sure changing scheme updates the default port + rv = NS_NewURI(getter_AddRefs(uri), + "https://example.org:80/path?query#ref"_ns); + ASSERT_EQ(rv, NS_OK); + rv = NS_MutateURI(uri).SetScheme("http"_ns).Finalize(uri); + ASSERT_EQ(rv, NS_OK); + rv = uri->GetSpec(out); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(out, "http://example.org/path?query#ref"_ns); + int32_t port; + rv = uri->GetPort(&port); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(port, -1); + rv = uri->GetFilePath(out); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(out, "/path"_ns); + rv = uri->GetQuery(out); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(out, "query"_ns); + rv = uri->GetRef(out); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(out, "ref"_ns); + + // Make sure changing scheme does not change non-default port + rv = NS_NewURI(getter_AddRefs(uri), "https://example.org:123"_ns); + ASSERT_EQ(rv, NS_OK); + rv = NS_MutateURI(uri).SetScheme("http"_ns).Finalize(uri); + ASSERT_EQ(rv, NS_OK); + rv = uri->GetSpec(out); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(out, "http://example.org:123/"_ns); + rv = uri->GetPort(&port); + ASSERT_EQ(rv, NS_OK); + ASSERT_EQ(port, 123); +} + +extern MOZ_THREAD_LOCAL(uint32_t) gTlsURLRecursionCount; + +TEST(TestURIMutator, OnAnyThread) +{ + nsCOMPtr<nsIThreadPool> pool = new nsThreadPool(); + pool->SetThreadLimit(60); + + pool = new nsThreadPool(); + for (int i = 0; i < 1000; ++i) { + nsCOMPtr<nsIRunnable> task = + NS_NewRunnableFunction("gtest-OnAnyThread", []() { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), "http://example.com"_ns); + ASSERT_EQ(rv, NS_OK); + nsAutoCString out; + ASSERT_EQ(uri->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "http://example.com/"_ns); + }); + EXPECT_TRUE(task); + + pool->Dispatch(task, NS_DISPATCH_NORMAL); + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), "http://example.com"_ns); + ASSERT_EQ(rv, NS_OK); + nsAutoCString out; + ASSERT_EQ(uri->GetSpec(out), NS_OK); + ASSERT_TRUE(out == "http://example.com/"_ns); + + pool->Shutdown(); + + ASSERT_EQ(gTlsURLRecursionCount.get(), 0u); +} diff --git a/netwerk/test/gtest/moz.build b/netwerk/test/gtest/moz.build new file mode 100644 index 0000000000..79ca936efb --- /dev/null +++ b/netwerk/test/gtest/moz.build @@ -0,0 +1,79 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES += [ + "TestBase64Stream.cpp", + "TestBind.cpp", + "TestBufferedInputStream.cpp", + "TestCommon.cpp", + "TestCookie.cpp", + "TestDNSPacket.cpp", + "TestHeaders.cpp", + "TestHttpAuthUtils.cpp", + "TestHttpChannel.cpp", + "TestHttpResponseHead.cpp", + "TestInputStreamTransport.cpp", + "TestIsValidIp.cpp", + "TestLinkHeader.cpp", + "TestMIMEInputStream.cpp", + "TestMozURL.cpp", + "TestProtocolProxyService.cpp", + "TestReadStreamToString.cpp", + "TestServerTimingHeader.cpp", + "TestSocketTransportService.cpp", + "TestSSLTokensCache.cpp", + "TestStandardURL.cpp", + "TestUDPSocket.cpp", +] + +if CONFIG["OS_TARGET"] == "WINNT": + UNIFIED_SOURCES += [ + "TestNamedPipeService.cpp", + ] + +# skip the test on windows10-aarch64 +if not (CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CPU_ARCH"] == "aarch64"): + UNIFIED_SOURCES += [ + "TestPACMan.cpp", + "TestURIMutator.cpp", + ] + +# run the test on windows only +if CONFIG["OS_TARGET"] == "WINNT": + UNIFIED_SOURCES += ["TestNetworkLinkIdHashingWindows.cpp"] + +# run the test on mac only +if CONFIG["OS_TARGET"] == "Darwin": + UNIFIED_SOURCES += ["TestNetworkLinkIdHashingDarwin.cpp"] + +TEST_HARNESS_FILES.gtest += [ + "urltestdata.json", +] + +USE_LIBS += [ + "jsoncpp", +] + +LOCAL_INCLUDES += [ + "/netwerk/base", + "/netwerk/cookie", + "/toolkit/components/jsoncpp/include", + "/xpcom/tests/gtest", +] + +# windows includes only +if CONFIG["OS_TARGET"] == "WINNT": + LOCAL_INCLUDES += ["/netwerk/system/win32"] + +# mac includes only +if CONFIG["OS_TARGET"] == "Darwin": + LOCAL_INCLUDES += ["/netwerk/system/mac"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += ["!/xpcom", "/xpcom/components"] diff --git a/netwerk/test/gtest/urltestdata-orig.json b/netwerk/test/gtest/urltestdata-orig.json new file mode 100644 index 0000000000..5565c938fd --- /dev/null +++ b/netwerk/test/gtest/urltestdata-orig.json @@ -0,0 +1,6148 @@ +[ + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/segments.js", + { + "input": "http://example\t.\norg", + "base": "http://example.org/foo/bar", + "href": "http://example.org/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://user:pass@foo:21/bar;par?b#c", + "base": "http://example.org/foo/bar", + "href": "http://user:pass@foo:21/bar;par?b#c", + "origin": "http://foo:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "foo:21", + "hostname": "foo", + "port": "21", + "pathname": "/bar;par", + "search": "?b", + "hash": "#c" + }, + { + "input": "https://test:@test", + "base": "about:blank", + "href": "https://test@test/", + "origin": "https://test", + "protocol": "https:", + "username": "test", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://:@test", + "base": "about:blank", + "href": "https://test/", + "origin": "https://test", + "protocol": "https:", + "username": "", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://test:@test/x", + "base": "about:blank", + "href": "non-special://test@test/x", + "origin": "null", + "protocol": "non-special:", + "username": "test", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/x", + "search": "", + "hash": "" + }, + { + "input": "non-special://:@test/x", + "base": "about:blank", + "href": "non-special://test/x", + "origin": "null", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/x", + "search": "", + "hash": "" + }, + { + "input": "http:foo.com", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/foo.com", + "search": "", + "hash": "" + }, + { + "input": "\t :foo.com \n", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com", + "search": "", + "hash": "" + }, + { + "input": " foo.com ", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/foo.com", + "search": "", + "hash": "" + }, + { + "input": "a:\t foo.com", + "base": "http://example.org/foo/bar", + "href": "a: foo.com", + "origin": "null", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": " foo.com", + "search": "", + "hash": "" + }, + { + "input": "http://f:21/ b ? d # e ", + "base": "http://example.org/foo/bar", + "href": "http://f:21/%20b%20?%20d%20# e", + "origin": "http://f:21", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:21", + "hostname": "f", + "port": "21", + "pathname": "/%20b%20", + "search": "?%20d%20", + "hash": "# e" + }, + { + "input": "lolscheme:x x#x x", + "base": "about:blank", + "href": "lolscheme:x x#x x", + "protocol": "lolscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "x x", + "search": "", + "hash": "#x x" + }, + { + "input": "http://f:/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:0/c", + "base": "http://example.org/foo/bar", + "href": "http://f:0/c", + "origin": "http://f:0", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:0", + "hostname": "f", + "port": "0", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:00000000000000/c", + "base": "http://example.org/foo/bar", + "href": "http://f:0/c", + "origin": "http://f:0", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:0", + "hostname": "f", + "port": "0", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:00000000000000000000080/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:b/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f: /c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f:\n/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:fifty-two/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f:999999/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "non-special://f:999999/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f: 21 / b ? d # e ", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": " \t", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": ":foo.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com/", + "search": "", + "hash": "" + }, + { + "input": ":foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com/", + "search": "", + "hash": "" + }, + { + "input": ":", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:", + "search": "", + "hash": "" + }, + { + "input": ":a", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:a", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:a", + "search": "", + "hash": "" + }, + { + "input": ":/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:/", + "search": "", + "hash": "" + }, + { + "input": ":\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:/", + "search": "", + "hash": "" + }, + { + "input": ":#", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:#", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:", + "search": "", + "hash": "" + }, + { + "input": "#", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "#/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#/" + }, + { + "input": "#\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#\\", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#\\" + }, + { + "input": "#;?", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#;?", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#;?" + }, + { + "input": "?", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar?", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": ":23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:23", + "search": "", + "hash": "" + }, + { + "input": "/:23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/:23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/:23", + "search": "", + "hash": "" + }, + { + "input": "::", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/::", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/::", + "search": "", + "hash": "" + }, + { + "input": "::23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/::23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/::23", + "search": "", + "hash": "" + }, + { + "input": "foo://", + "base": "http://example.org/foo/bar", + "href": "foo:///", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://a:b@c:29/d", + "base": "http://example.org/foo/bar", + "href": "http://a:b@c:29/d", + "origin": "http://c:29", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "c:29", + "hostname": "c", + "port": "29", + "pathname": "/d", + "search": "", + "hash": "" + }, + { + "input": "http::@c:29", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:@c:29", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:@c:29", + "search": "", + "hash": "" + }, + { + "input": "http://&a:foo(b]c@d:2/", + "base": "http://example.org/foo/bar", + "href": "http://&a:foo(b%5Dc@d:2/", + "origin": "http://d:2", + "protocol": "http:", + "username": "&a", + "password": "foo(b%5Dc", + "host": "d:2", + "hostname": "d", + "port": "2", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://::@c@d:2", + "base": "http://example.org/foo/bar", + "href": "http://:%3A%40c@d:2/", + "origin": "http://d:2", + "protocol": "http:", + "username": "", + "password": "%3A%40c", + "host": "d:2", + "hostname": "d", + "port": "2", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo.com:b@d/", + "base": "http://example.org/foo/bar", + "href": "http://foo.com:b@d/", + "origin": "http://d", + "protocol": "http:", + "username": "foo.com", + "password": "b", + "host": "d", + "hostname": "d", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo.com/\\@", + "base": "http://example.org/foo/bar", + "href": "http://foo.com//@", + "origin": "http://foo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.com", + "hostname": "foo.com", + "port": "", + "pathname": "//@", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://foo.com/", + "origin": "http://foo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.com", + "hostname": "foo.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\a\\b:c\\d@foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://a/b:c/d@foo.com/", + "origin": "http://a", + "protocol": "http:", + "username": "", + "password": "", + "host": "a", + "hostname": "a", + "port": "", + "pathname": "/b:c/d@foo.com/", + "search": "", + "hash": "" + }, + { + "input": "foo:/", + "base": "http://example.org/foo/bar", + "href": "foo:/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "foo:/bar.com/", + "base": "http://example.org/foo/bar", + "href": "foo:/bar.com/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/bar.com/", + "search": "", + "hash": "" + }, + { + "input": "foo://///////", + "base": "http://example.org/foo/bar", + "href": "foo://///////", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "///////", + "search": "", + "hash": "" + }, + { + "input": "foo://///////bar.com/", + "base": "http://example.org/foo/bar", + "href": "foo://///////bar.com/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "///////bar.com/", + "search": "", + "hash": "" + }, + { + "input": "foo:////://///", + "base": "http://example.org/foo/bar", + "href": "foo:////://///", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "//://///", + "search": "", + "hash": "" + }, + { + "input": "c:/foo", + "base": "http://example.org/foo/bar", + "href": "c:/foo", + "origin": "null", + "protocol": "c:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "//foo/bar", + "base": "http://example.org/foo/bar", + "href": "http://foo/bar", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/bar", + "search": "", + "hash": "" + }, + { + "input": "http://foo/path;a??e#f#g", + "base": "http://example.org/foo/bar", + "href": "http://foo/path;a??e#f#g", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/path;a", + "search": "??e", + "hash": "#f#g" + }, + { + "input": "http://foo/abcd?efgh?ijkl", + "base": "http://example.org/foo/bar", + "href": "http://foo/abcd?efgh?ijkl", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/abcd", + "search": "?efgh?ijkl", + "hash": "" + }, + { + "input": "http://foo/abcd#foo?bar", + "base": "http://example.org/foo/bar", + "href": "http://foo/abcd#foo?bar", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/abcd", + "search": "", + "hash": "#foo?bar" + }, + { + "input": "[61:24:74]:98", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/[61:24:74]:98", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/[61:24:74]:98", + "search": "", + "hash": "" + }, + { + "input": "http:[61:27]/:foo", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/[61:27]/:foo", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/[61:27]/:foo", + "search": "", + "hash": "" + }, + { + "input": "http://[1::2]:3:4", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1]", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1]:80", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://[2001::1]", + "base": "http://example.org/foo/bar", + "href": "http://[2001::1]/", + "origin": "http://[2001::1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[2001::1]", + "hostname": "[2001::1]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[::127.0.0.1]", + "base": "http://example.org/foo/bar", + "href": "http://[::7f00:1]/", + "origin": "http://[::7f00:1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[::7f00:1]", + "hostname": "[::7f00:1]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[0:0:0:0:0:0:13.1.68.3]", + "base": "http://example.org/foo/bar", + "href": "http://[::d01:4403]/", + "origin": "http://[::d01:4403]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[::d01:4403]", + "hostname": "[::d01:4403]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[2001::1]:80", + "base": "http://example.org/foo/bar", + "href": "http://[2001::1]/", + "origin": "http://[2001::1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[2001::1]", + "hostname": "[2001::1]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/example.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/example.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftp:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https:/example.com/", + "base": "http://example.org/foo/bar", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "madeupscheme:/example.com/", + "base": "http://example.org/foo/bar", + "href": "madeupscheme:/example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file:/example.com/", + "base": "http://example.org/foo/bar", + "href": "file:///example.com/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file://example:1/", + "base": "about:blank", + "failure": true + }, + { + "input": "file://example:test/", + "base": "about:blank", + "failure": true + }, + { + "input": "file://example%/", + "base": "about:blank", + "failure": true + }, + { + "input": "file://[example]/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftps:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ftps:/example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:/example.com/", + "base": "http://example.org/foo/bar", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss:/example.com/", + "base": "http://example.org/foo/bar", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "data:/example.com/", + "base": "http://example.org/foo/bar", + "href": "data:/example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "javascript:/example.com/", + "base": "http://example.org/foo/bar", + "href": "javascript:/example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/example.com/", + "base": "http://example.org/foo/bar", + "href": "mailto:/example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "http:example.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/example.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftp:example.com/", + "base": "http://example.org/foo/bar", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https:example.com/", + "base": "http://example.org/foo/bar", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "madeupscheme:example.com/", + "base": "http://example.org/foo/bar", + "href": "madeupscheme:example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:example.com/", + "base": "http://example.org/foo/bar", + "href": "ftps:example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:example.com/", + "base": "http://example.org/foo/bar", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws:example.com/", + "base": "http://example.org/foo/bar", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss:example.com/", + "base": "http://example.org/foo/bar", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "data:example.com/", + "base": "http://example.org/foo/bar", + "href": "data:example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "javascript:example.com/", + "base": "http://example.org/foo/bar", + "href": "javascript:example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:example.com/", + "base": "http://example.org/foo/bar", + "href": "mailto:example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "/a/b/c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/b/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/b/c", + "search": "", + "hash": "" + }, + { + "input": "/a/ /c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/%20/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/%20/c", + "search": "", + "hash": "" + }, + { + "input": "/a%2fc", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a%2fc", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a%2fc", + "search": "", + "hash": "" + }, + { + "input": "/a/%2f/c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/%2f/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/%2f/c", + "search": "", + "hash": "" + }, + { + "input": "#β", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#%CE%B2", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#%CE%B2" + }, + { + "input": "data:text/html,test#test", + "base": "http://example.org/foo/bar", + "href": "data:text/html,test#test", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "text/html,test", + "search": "", + "hash": "#test" + }, + { + "input": "tel:1234567890", + "base": "http://example.org/foo/bar", + "href": "tel:1234567890", + "origin": "null", + "protocol": "tel:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "1234567890", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/file.html", + { + "input": "file:c:\\foo\\bar.html", + "base": "file:///tmp/mock/path", + "href": "file:///c:/foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:/foo/bar.html", + "search": "", + "hash": "" + }, + { + "input": " File:c|////foo\\bar.html", + "base": "file:///tmp/mock/path", + "href": "file:///c:////foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:////foo/bar.html", + "search": "", + "hash": "" + }, + { + "input": "C|/foo/bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "/C|\\foo\\bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "//C|/foo/bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "//server/file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "server", + "hostname": "server", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "\\\\server\\file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "server", + "hostname": "server", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "/\\server/file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "server", + "hostname": "server", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "file:///foo/bar.txt", + "base": "file:///tmp/mock/path", + "href": "file:///foo/bar.txt", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/foo/bar.txt", + "search": "", + "hash": "" + }, + { + "input": "file:///home/me", + "base": "file:///tmp/mock/path", + "href": "file:///home/me", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/home/me", + "search": "", + "hash": "" + }, + { + "input": "//", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "///", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "///test", + "base": "file:///tmp/mock/path", + "href": "file:///test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "file://test", + "base": "file:///tmp/mock/path", + "href": "file://test/", + "protocol": "file:", + "username": "", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost/", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost/test", + "base": "file:///tmp/mock/path", + "href": "file:///test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "test", + "base": "file:///tmp/mock/path", + "href": "file:///tmp/mock/test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/tmp/mock/test", + "search": "", + "hash": "" + }, + { + "input": "file:test", + "base": "file:///tmp/mock/path", + "href": "file:///tmp/mock/test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/tmp/mock/test", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/path.js", + { + "input": "http://example.com/././foo", + "base": "about:blank", + "href": "http://example.com/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/./.foo", + "base": "about:blank", + "href": "http://example.com/.foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/.foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/.", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/./", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/..", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/..bar", + "base": "about:blank", + "href": "http://example.com/foo/..bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/..bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../ton", + "base": "about:blank", + "href": "http://example.com/foo/ton", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/ton", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../ton/../../a", + "base": "about:blank", + "href": "http://example.com/a", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/a", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/../../..", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/../../../ton", + "base": "about:blank", + "href": "http://example.com/ton", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/ton", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e%2", + "base": "about:blank", + "href": "http://example.com/foo/%2e%2", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/%2e%2", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar", + "base": "about:blank", + "href": "http://example.com/%2e.bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%2e.bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com////../..", + "base": "about:blank", + "href": "http://example.com//", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "//", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar//../..", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar//..", + "base": "about:blank", + "href": "http://example.com/foo/bar/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/bar/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo", + "base": "about:blank", + "href": "http://example.com/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%20foo", + "base": "about:blank", + "href": "http://example.com/%20foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%20foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%", + "base": "about:blank", + "href": "http://example.com/foo%", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%2", + "base": "about:blank", + "href": "http://example.com/foo%2", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%2zbar", + "base": "about:blank", + "href": "http://example.com/foo%2zbar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2zbar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%2©zbar", + "base": "about:blank", + "href": "http://example.com/foo%2%C3%82%C2%A9zbar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2%C3%82%C2%A9zbar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%41%7a", + "base": "about:blank", + "href": "http://example.com/foo%41%7a", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%41%7a", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo\t\u0091%91", + "base": "about:blank", + "href": "http://example.com/foo%C2%91%91", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%C2%91%91", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%00%51", + "base": "about:blank", + "href": "http://example.com/foo%00%51", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%00%51", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/(%28:%3A%29)", + "base": "about:blank", + "href": "http://example.com/(%28:%3A%29)", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/(%28:%3A%29)", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%3A%3a%3C%3c", + "base": "about:blank", + "href": "http://example.com/%3A%3a%3C%3c", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%3A%3a%3C%3c", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo\tbar", + "base": "about:blank", + "href": "http://example.com/foobar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foobar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com\\\\foo\\\\bar", + "base": "about:blank", + "href": "http://example.com//foo//bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "//foo//bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd", + "base": "about:blank", + "href": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%7Ffp3%3Eju%3Dduvgw%3Dd", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/@asdf%40", + "base": "about:blank", + "href": "http://example.com/@asdf%40", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/@asdf%40", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/你好你好", + "base": "about:blank", + "href": "http://example.com/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/‥/foo", + "base": "about:blank", + "href": "http://example.com/%E2%80%A5/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E2%80%A5/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com//foo", + "base": "about:blank", + "href": "http://example.com/%EF%BB%BF/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%EF%BB%BF/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com//foo//bar", + "base": "about:blank", + "href": "http://example.com/%E2%80%AE/foo/%E2%80%AD/bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E2%80%AE/foo/%E2%80%AD/bar", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/relative.js", + { + "input": "http://www.google.com/foo?bar=baz#", + "base": "about:blank", + "href": "http://www.google.com/foo?bar=baz#", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "?bar=baz", + "hash": "" + }, + { + "input": "http://www.google.com/foo?bar=baz# »", + "base": "about:blank", + "href": "http://www.google.com/foo?bar=baz# %C2%BB", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "?bar=baz", + "hash": "# %C2%BB" + }, + { + "input": "data:test# »", + "base": "about:blank", + "href": "data:test# %C2%BB", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "test", + "search": "", + "hash": "# %C2%BB" + }, + { + "input": "http://www.google.com", + "base": "about:blank", + "href": "http://www.google.com/", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.0x00A80001", + "base": "about:blank", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.168.0.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://www/foo%2Ehtml", + "base": "about:blank", + "href": "http://www/foo%2Ehtml", + "origin": "http://www", + "protocol": "http:", + "username": "", + "password": "", + "host": "www", + "hostname": "www", + "port": "", + "pathname": "/foo%2Ehtml", + "search": "", + "hash": "" + }, + { + "input": "http://www/foo/%2E/html", + "base": "about:blank", + "href": "http://www/foo/html", + "origin": "http://www", + "protocol": "http:", + "username": "", + "password": "", + "host": "www", + "hostname": "www", + "port": "", + "pathname": "/foo/html", + "search": "", + "hash": "" + }, + { + "input": "http://user:pass@/", + "base": "about:blank", + "failure": true + }, + { + "input": "http://%25DOMAIN:foobar@foodomain.com/", + "base": "about:blank", + "href": "http://%25DOMAIN:foobar@foodomain.com/", + "origin": "http://foodomain.com", + "protocol": "http:", + "username": "%25DOMAIN", + "password": "foobar", + "host": "foodomain.com", + "hostname": "foodomain.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\www.google.com\\foo", + "base": "about:blank", + "href": "http://www.google.com/foo", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://foo:80/", + "base": "about:blank", + "href": "http://foo/", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo:81/", + "base": "about:blank", + "href": "http://foo:81/", + "origin": "http://foo:81", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "httpa://foo:80/", + "base": "about:blank", + "href": "httpa://foo:80/", + "origin": "null", + "protocol": "httpa:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo:-80/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://foo:443/", + "base": "about:blank", + "href": "https://foo/", + "origin": "https://foo", + "protocol": "https:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://foo:80/", + "base": "about:blank", + "href": "https://foo:80/", + "origin": "https://foo:80", + "protocol": "https:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp://foo:21/", + "base": "about:blank", + "href": "ftp://foo/", + "origin": "ftp://foo", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp://foo:80/", + "base": "about:blank", + "href": "ftp://foo:80/", + "origin": "ftp://foo:80", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "gopher://foo:70/", + "base": "about:blank", + "href": "gopher://foo/", + "origin": "gopher://foo", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "gopher://foo:443/", + "base": "about:blank", + "href": "gopher://foo:443/", + "origin": "gopher://foo:443", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "foo:443", + "hostname": "foo", + "port": "443", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:80/", + "base": "about:blank", + "href": "ws://foo/", + "origin": "ws://foo", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:81/", + "base": "about:blank", + "href": "ws://foo:81/", + "origin": "ws://foo:81", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:443/", + "base": "about:blank", + "href": "ws://foo:443/", + "origin": "ws://foo:443", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:443", + "hostname": "foo", + "port": "443", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:815/", + "base": "about:blank", + "href": "ws://foo:815/", + "origin": "ws://foo:815", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:815", + "hostname": "foo", + "port": "815", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:80/", + "base": "about:blank", + "href": "wss://foo:80/", + "origin": "wss://foo:80", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:81/", + "base": "about:blank", + "href": "wss://foo:81/", + "origin": "wss://foo:81", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:443/", + "base": "about:blank", + "href": "wss://foo/", + "origin": "wss://foo", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:815/", + "base": "about:blank", + "href": "wss://foo:815/", + "origin": "wss://foo:815", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:815", + "hostname": "foo", + "port": "815", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/example.com/", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp:/example.com/", + "base": "about:blank", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https:/example.com/", + "base": "about:blank", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "madeupscheme:/example.com/", + "base": "about:blank", + "href": "madeupscheme:/example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file:/example.com/", + "base": "about:blank", + "href": "file:///example.com/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:/example.com/", + "base": "about:blank", + "href": "ftps:/example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:/example.com/", + "base": "about:blank", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws:/example.com/", + "base": "about:blank", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss:/example.com/", + "base": "about:blank", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "data:/example.com/", + "base": "about:blank", + "href": "data:/example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "javascript:/example.com/", + "base": "about:blank", + "href": "javascript:/example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/example.com/", + "base": "about:blank", + "href": "mailto:/example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "http:example.com/", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp:example.com/", + "base": "about:blank", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https:example.com/", + "base": "about:blank", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "madeupscheme:example.com/", + "base": "about:blank", + "href": "madeupscheme:example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:example.com/", + "base": "about:blank", + "href": "ftps:example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:example.com/", + "base": "about:blank", + "href": "gopher://example.com/", + "origin": "gopher://example.com", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws:example.com/", + "base": "about:blank", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss:example.com/", + "base": "about:blank", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "data:example.com/", + "base": "about:blank", + "href": "data:example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "javascript:example.com/", + "base": "about:blank", + "href": "javascript:example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:example.com/", + "base": "about:blank", + "href": "mailto:example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/segments-userinfo-vs-host.html", + { + "input": "http:@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://@pple.com", + "base": "about:blank", + "href": "http://pple.com/", + "origin": "http://pple.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "pple.com", + "hostname": "pple.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http::b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/:b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://:b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/:@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://user@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:/@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "https:@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:a:b@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:/a:b@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://a:b@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http::@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://www.@pple.com", + "base": "about:blank", + "href": "http://www.@pple.com/", + "origin": "http://pple.com", + "protocol": "http:", + "username": "www.", + "password": "", + "host": "pple.com", + "hostname": "pple.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:@:www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:/@:www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://@:www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://:@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# Others", + { + "input": "/", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": ".", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "./test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../aaa/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/aaa/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/aaa/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../../test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "中/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/%E4%B8%AD/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/%E4%B8%AD/test.txt", + "search": "", + "hash": "" + }, + { + "input": "http://www.example2.com", + "base": "http://www.example.com/test", + "href": "http://www.example2.com/", + "origin": "http://www.example2.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example2.com", + "hostname": "www.example2.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "//www.example2.com", + "base": "http://www.example.com/test", + "href": "http://www.example2.com/", + "origin": "http://www.example2.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example2.com", + "hostname": "www.example2.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:...", + "base": "http://www.example.com/test", + "href": "file:///...", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/...", + "search": "", + "hash": "" + }, + { + "input": "file:..", + "base": "http://www.example.com/test", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:a", + "base": "http://www.example.com/test", + "href": "file:///a", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/a", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/host.html", + "Basic canonicalization, uppercase should be converted to lowercase", + { + "input": "http://ExAmPlE.CoM", + "base": "http://other.com/", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://example example.com", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://Goo%20 goo%7C|.com", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[:]", + "base": "http://other.com/", + "failure": true + }, + "U+3000 is mapped to U+0020 (space) which is disallowed", + { + "input": "http://GOO\u00a0\u3000goo.com", + "base": "http://other.com/", + "failure": true + }, + "Other types of space (no-break, zero-width, zero-width-no-break) are name-prepped away to nothing. U+200B, U+2060, and U+FEFF, are ignored", + { + "input": "http://GOO\u200b\u2060\ufeffgoo.com", + "base": "http://other.com/", + "href": "http://googoo.com/", + "origin": "http://googoo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "googoo.com", + "hostname": "googoo.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Leading and trailing C0 control or space", + { + "input": "\u0000\u001b\u0004\u0012 http://example.com/\u001f \u000d ", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Ideographic full stop (full-width period for Chinese, etc.) should be treated as a dot. U+3002 is mapped to U+002E (dot)", + { + "input": "http://www.foo。bar.com", + "base": "http://other.com/", + "href": "http://www.foo.bar.com/", + "origin": "http://www.foo.bar.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.foo.bar.com", + "hostname": "www.foo.bar.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid unicode characters should fail... U+FDD0 is disallowed; %ef%b7%90 is U+FDD0", + { + "input": "http://\ufdd0zyx.com", + "base": "http://other.com/", + "failure": true + }, + "This is the same as previous but escaped", + { + "input": "http://%ef%b7%90zyx.com", + "base": "http://other.com/", + "failure": true + }, + "U+FFFD", + { + "input": "https://\ufffd", + "base": "about:blank", + "failure": true + }, + { + "input": "https://%EF%BF%BD", + "base": "about:blank", + "failure": true + }, + { + "input": "https://x/\ufffd?\ufffd#\ufffd", + "base": "about:blank", + "href": "https://x/%EF%BF%BD?%EF%BF%BD#%EF%BF%BD", + "origin": "https://x", + "protocol": "https:", + "username": "", + "password": "", + "host": "x", + "hostname": "x", + "port": "", + "pathname": "/%EF%BF%BD", + "search": "?%EF%BF%BD", + "hash": "#%EF%BF%BD" + }, + "Test name prepping, fullwidth input should be converted to ASCII and NOT IDN-ized. This is 'Go' in fullwidth UTF-8/UTF-16.", + { + "input": "http://Go.com", + "base": "http://other.com/", + "href": "http://go.com/", + "origin": "http://go.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "go.com", + "hostname": "go.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "URL spec forbids the following. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24257", + { + "input": "http://%41.com", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://%ef%bc%85%ef%bc%94%ef%bc%91.com", + "base": "http://other.com/", + "failure": true + }, + "...%00 in fullwidth should fail (also as escaped UTF-8 input)", + { + "input": "http://%00.com", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://%ef%bc%85%ef%bc%90%ef%bc%90.com", + "base": "http://other.com/", + "failure": true + }, + "Basic IDN support, UTF-8 and UTF-16 input should be converted to IDN", + { + "input": "http://你好你好", + "base": "http://other.com/", + "href": "http://xn--6qqa088eba/", + "origin": "http://xn--6qqa088eba", + "protocol": "http:", + "username": "", + "password": "", + "host": "xn--6qqa088eba", + "hostname": "xn--6qqa088eba", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://faß.ExAmPlE/", + "base": "about:blank", + "href": "https://xn--fa-hia.example/", + "origin": "https://xn--fa-hia.example", + "protocol": "https:", + "username": "", + "password": "", + "host": "xn--fa-hia.example", + "hostname": "xn--fa-hia.example", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://faß.ExAmPlE/", + "base": "about:blank", + "href": "sc://fa%C3%9F.ExAmPlE/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "fa%C3%9F.ExAmPlE", + "hostname": "fa%C3%9F.ExAmPlE", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid escaped characters should fail and the percents should be escaped. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24191", + { + "input": "http://%zz%66%a.com", + "base": "http://other.com/", + "failure": true + }, + "If we get an invalid character that has been escaped.", + { + "input": "http://%25", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://hello%00", + "base": "http://other.com/", + "failure": true + }, + "Escaped numbers should be treated like IP addresses if they are.", + { + "input": "http://%30%78%63%30%2e%30%32%35%30.01", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.168.0.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://%30%78%63%30%2e%30%32%35%30.01%2e", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.168.0.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.168.0.257", + "base": "http://other.com/", + "failure": true + }, + "Invalid escaping in hosts causes failure", + { + "input": "http://%3g%78%63%30%2e%30%32%35%30%2E.01", + "base": "http://other.com/", + "failure": true + }, + "A space in a host causes failure", + { + "input": "http://192.168.0.1 hello", + "base": "http://other.com/", + "failure": true + }, + { + "input": "https://x x:12", + "base": "about:blank", + "failure": true + }, + "Fullwidth and escaped UTF-8 fullwidth should still be treated as IP", + { + "input": "http://0Xc0.0250.01", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.168.0.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Domains with empty labels", + { + "input": "http://./", + "base": "about:blank", + "href": "http://./", + "origin": "http://.", + "protocol": "http:", + "username": "", + "password": "", + "host": ".", + "hostname": ".", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://../", + "base": "about:blank", + "href": "http://../", + "origin": "http://..", + "protocol": "http:", + "username": "", + "password": "", + "host": "..", + "hostname": "..", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0..0x300/", + "base": "about:blank", + "href": "http://0..0x300/", + "origin": "http://0..0x300", + "protocol": "http:", + "username": "", + "password": "", + "host": "0..0x300", + "hostname": "0..0x300", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Broken IPv6", + { + "input": "http://[www.google.com]/", + "base": "about:blank", + "failure": true + }, + { + "input": "http://[google.com]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[::1.2.3.4x]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[::1.2.3.]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[::1.2.]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[::1.]", + "base": "http://other.com/", + "failure": true + }, + "Misc Unicode", + { + "input": "http://foo:💩@example.com/bar", + "base": "http://other.com/", + "href": "http://foo:%F0%9F%92%A9@example.com/bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "foo", + "password": "%F0%9F%92%A9", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/bar", + "search": "", + "hash": "" + }, + "# resolving a fragment against any scheme succeeds", + { + "input": "#", + "base": "test:test", + "href": "test:test#", + "origin": "null", + "protocol": "test:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "test", + "search": "", + "hash": "" + }, + { + "input": "#x", + "base": "mailto:x@x.com", + "href": "mailto:x@x.com#x", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "x@x.com", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "data:,", + "href": "data:,#x", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": ",", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "about:blank", + "href": "about:blank#x", + "origin": "null", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "#x" + }, + { + "input": "#", + "base": "test:test?test", + "href": "test:test?test#", + "origin": "null", + "protocol": "test:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "test", + "search": "?test", + "hash": "" + }, + "# multiple @ in authority state", + { + "input": "https://@test@test@example:800/", + "base": "http://doesnotmatter/", + "href": "https://%40test%40test@example:800/", + "origin": "https://example:800", + "protocol": "https:", + "username": "%40test%40test", + "password": "", + "host": "example:800", + "hostname": "example", + "port": "800", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://@@@example", + "base": "http://doesnotmatter/", + "href": "https://%40%40@example/", + "origin": "https://example", + "protocol": "https:", + "username": "%40%40", + "password": "", + "host": "example", + "hostname": "example", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "non-az-09 characters", + { + "input": "http://`{}:`{}@h/`{}?`{}", + "base": "http://doesnotmatter/", + "href": "http://%60%7B%7D:%60%7B%7D@h/%60%7B%7D?`{}", + "origin": "http://h", + "protocol": "http:", + "username": "%60%7B%7D", + "password": "%60%7B%7D", + "host": "h", + "hostname": "h", + "port": "", + "pathname": "/%60%7B%7D", + "search": "?`{}", + "hash": "" + }, + "# Credentials in base", + { + "input": "/some/path", + "base": "http://user@example.org/smth", + "href": "http://user@example.org/some/path", + "origin": "http://example.org", + "protocol": "http:", + "username": "user", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/some/path", + "search": "", + "hash": "" + }, + { + "input": "", + "base": "http://user:pass@example.org:21/smth", + "href": "http://user:pass@example.org:21/smth", + "origin": "http://example.org:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "example.org:21", + "hostname": "example.org", + "port": "21", + "pathname": "/smth", + "search": "", + "hash": "" + }, + { + "input": "/some/path", + "base": "http://user:pass@example.org:21/smth", + "href": "http://user:pass@example.org:21/some/path", + "origin": "http://example.org:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "example.org:21", + "hostname": "example.org", + "port": "21", + "pathname": "/some/path", + "search": "", + "hash": "" + }, + "# a set of tests designed by zcorpan for relative URLs with unknown schemes", + { + "input": "i", + "base": "sc:sd", + "failure": true + }, + { + "input": "i", + "base": "sc:sd/sd", + "failure": true + }, + { + "input": "i", + "base": "sc:/pa/pa", + "href": "sc:/pa/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/i", + "search": "", + "hash": "" + }, + { + "input": "i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "i", + "base": "sc:///pa/pa", + "href": "sc:///pa/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/i", + "search": "", + "hash": "" + }, + { + "input": "../i", + "base": "sc:sd", + "failure": true + }, + { + "input": "../i", + "base": "sc:sd/sd", + "failure": true + }, + { + "input": "../i", + "base": "sc:/pa/pa", + "href": "sc:/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "../i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "../i", + "base": "sc:///pa/pa", + "href": "sc:///i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "/i", + "base": "sc:sd", + "failure": true + }, + { + "input": "/i", + "base": "sc:sd/sd", + "failure": true + }, + { + "input": "/i", + "base": "sc:/pa/pa", + "href": "sc:/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "/i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "/i", + "base": "sc:///pa/pa", + "href": "sc:///i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/i", + "search": "", + "hash": "" + }, + { + "input": "?i", + "base": "sc:sd", + "failure": true + }, + { + "input": "?i", + "base": "sc:sd/sd", + "failure": true + }, + { + "input": "?i", + "base": "sc:/pa/pa", + "href": "sc:/pa/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/pa", + "search": "?i", + "hash": "" + }, + { + "input": "?i", + "base": "sc://ho/pa", + "href": "sc://ho/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/pa", + "search": "?i", + "hash": "" + }, + { + "input": "?i", + "base": "sc:///pa/pa", + "href": "sc:///pa/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/pa", + "search": "?i", + "hash": "" + }, + { + "input": "#i", + "base": "sc:sd", + "href": "sc:sd#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "sd", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:sd/sd", + "href": "sc:sd/sd#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "sd/sd", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:/pa/pa", + "href": "sc:/pa/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/pa", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc://ho/pa", + "href": "sc://ho/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/pa", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:///pa/pa", + "href": "sc:///pa/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/pa", + "search": "", + "hash": "#i" + }, + "# make sure that relative URL logic works on known typically non-relative schemes too", + { + "input": "about:/../", + "base": "about:blank", + "href": "about:/", + "origin": "null", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "data:/../", + "base": "about:blank", + "href": "data:/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "javascript:/../", + "base": "about:blank", + "href": "javascript:/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/../", + "base": "about:blank", + "href": "mailto:/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# unknown schemes and their hosts", + { + "input": "sc://ñ.test/", + "base": "about:blank", + "href": "sc://%C3%B1.test/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1.test", + "hostname": "%C3%B1.test", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://\u001F!\"$&'()*+,-.;<=>^_`{|}~/", + "base": "about:blank", + "href": "sc://%1F!\"$&'()*+,-.;<=>^_`{|}~/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%1F!\"$&'()*+,-.;<=>^_`{|}~", + "hostname": "%1F!\"$&'()*+,-.;<=>^_`{|}~", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://\u0000/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc:// /", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://%/", + "base": "about:blank", + "href": "sc://%/", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%", + "hostname": "%", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://[/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://\\/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://]/", + "base": "about:blank", + "failure": true + }, + { + "input": "x", + "base": "sc://ñ", + "href": "sc://%C3%B1/x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": "%C3%B1", + "port": "", + "pathname": "/x", + "search": "", + "hash": "" + }, + "# unknown schemes and backslashes", + { + "input": "sc:\\../", + "base": "about:blank", + "href": "sc:\\../", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "\\../", + "search": "", + "hash": "" + }, + "# unknown scheme with path looking like a password", + { + "input": "sc::a@example.net", + "base": "about:blank", + "href": "sc::a@example.net", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": ":a@example.net", + "search": "", + "hash": "" + }, + "# unknown scheme with bogus percent-encoding", + { + "input": "wow:%NBD", + "base": "about:blank", + "href": "wow:%NBD", + "origin": "null", + "protocol": "wow:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "%NBD", + "search": "", + "hash": "" + }, + { + "input": "wow:%1G", + "base": "about:blank", + "href": "wow:%1G", + "origin": "null", + "protocol": "wow:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "%1G", + "search": "", + "hash": "" + }, + "# Hosts and percent-encoding", + { + "input": "ftp://example.com%80/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftp://example.com%A0/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://example.com%80/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://example.com%A0/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftp://%e2%98%83", + "base": "about:blank", + "href": "ftp://xn--n3h/", + "origin": "ftp://xn--n3h", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "xn--n3h", + "hostname": "xn--n3h", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://%e2%98%83", + "base": "about:blank", + "href": "https://xn--n3h/", + "origin": "https://xn--n3h", + "protocol": "https:", + "username": "", + "password": "", + "host": "xn--n3h", + "hostname": "xn--n3h", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# tests from jsdom/whatwg-url designed for code coverage", + { + "input": "http://127.0.0.1:10100/relative_import.html", + "base": "about:blank", + "href": "http://127.0.0.1:10100/relative_import.html", + "origin": "http://127.0.0.1:10100", + "protocol": "http:", + "username": "", + "password": "", + "host": "127.0.0.1:10100", + "hostname": "127.0.0.1", + "port": "10100", + "pathname": "/relative_import.html", + "search": "", + "hash": "" + }, + { + "input": "http://facebook.com/?foo=%7B%22abc%22", + "base": "about:blank", + "href": "http://facebook.com/?foo=%7B%22abc%22", + "origin": "http://facebook.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "facebook.com", + "hostname": "facebook.com", + "port": "", + "pathname": "/", + "search": "?foo=%7B%22abc%22", + "hash": "" + }, + { + "input": "https://localhost:3000/jqueryui@1.2.3", + "base": "about:blank", + "href": "https://localhost:3000/jqueryui@1.2.3", + "origin": "https://localhost:3000", + "protocol": "https:", + "username": "", + "password": "", + "host": "localhost:3000", + "hostname": "localhost", + "port": "3000", + "pathname": "/jqueryui@1.2.3", + "search": "", + "hash": "" + }, + "# tab/LF/CR", + { + "input": "h\tt\nt\rp://h\to\ns\rt:9\t0\n0\r0/p\ta\nt\rh?q\tu\ne\rry#f\tr\na\rg", + "base": "about:blank", + "href": "http://host:9000/path?query#frag", + "origin": "http://host:9000", + "protocol": "http:", + "username": "", + "password": "", + "host": "host:9000", + "hostname": "host", + "port": "9000", + "pathname": "/path", + "search": "?query", + "hash": "#frag" + }, + "# Stringification of URL.searchParams", + { + "input": "?a=b&c=d", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar?a=b&c=d", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "?a=b&c=d", + "searchParams": "a=b&c=d", + "hash": "" + }, + { + "input": "??a=b&c=d", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar??a=b&c=d", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "??a=b&c=d", + "searchParams": "%3Fa=b&c=d", + "hash": "" + }, + "# Scheme only", + { + "input": "http:", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "searchParams": "", + "hash": "" + }, + { + "input": "http:", + "base": "https://example.org/foo/bar", + "failure": true + }, + { + "input": "sc:", + "base": "https://example.org/foo/bar", + "href": "sc:", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "", + "search": "", + "searchParams": "", + "hash": "" + }, + "# Percent encoding of fragments", + { + "input": "http://foo.bar/baz?qux#foo\bbar", + "base": "about:blank", + "href": "http://foo.bar/baz?qux#foo%08bar", + "origin": "http://foo.bar", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.bar", + "hostname": "foo.bar", + "port": "", + "pathname": "/baz", + "search": "?qux", + "searchParams": "qux=", + "hash": "#foo%08bar" + }, + "# IPv4 parsing (via https://github.com/nodejs/node/pull/10317)", + { + "input": "http://192.168.257", + "base": "http://other.com/", + "href": "http://192.168.1.1/", + "origin": "http://192.168.1.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.1.1", + "hostname": "192.168.1.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.168.257.com", + "base": "http://other.com/", + "href": "http://192.168.257.com/", + "origin": "http://192.168.257.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.257.com", + "hostname": "192.168.257.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://256", + "base": "http://other.com/", + "href": "http://0.0.1.0/", + "origin": "http://0.0.1.0", + "protocol": "http:", + "username": "", + "password": "", + "host": "0.0.1.0", + "hostname": "0.0.1.0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://256.com", + "base": "http://other.com/", + "href": "http://256.com/", + "origin": "http://256.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "256.com", + "hostname": "256.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://999999999", + "base": "http://other.com/", + "href": "http://59.154.201.255/", + "origin": "http://59.154.201.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "59.154.201.255", + "hostname": "59.154.201.255", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://999999999.com", + "base": "http://other.com/", + "href": "http://999999999.com/", + "origin": "http://999999999.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "999999999.com", + "hostname": "999999999.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://10000000000", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://10000000000.com", + "base": "http://other.com/", + "href": "http://10000000000.com/", + "origin": "http://10000000000.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "10000000000.com", + "hostname": "10000000000.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://4294967295", + "base": "http://other.com/", + "href": "http://255.255.255.255/", + "origin": "http://255.255.255.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "255.255.255.255", + "hostname": "255.255.255.255", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://4294967296", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://0xffffffff", + "base": "http://other.com/", + "href": "http://255.255.255.255/", + "origin": "http://255.255.255.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "255.255.255.255", + "hostname": "255.255.255.255", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0xffffffff1", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://256.256.256.256", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://256.256.256.256.256", + "base": "http://other.com/", + "href": "http://256.256.256.256.256/", + "origin": "http://256.256.256.256.256", + "protocol": "http:", + "username": "", + "password": "", + "host": "256.256.256.256.256", + "hostname": "256.256.256.256.256", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://0x.0x.0", + "base": "about:blank", + "href": "https://0.0.0.0/", + "origin": "https://0.0.0.0", + "protocol": "https:", + "username": "", + "password": "", + "host": "0.0.0.0", + "hostname": "0.0.0.0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "More IPv4 parsing (via https://github.com/jsdom/whatwg-url/issues/92)", + { + "input": "https://256.0.0.1/test", + "base": "about:blank", + "failure": true + }, + "# file URLs containing percent-encoded Windows drive letters (shouldn't work)", + { + "input": "file:///C%3A/", + "base": "about:blank", + "href": "file:///C%3A/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C%3A/", + "search": "", + "hash": "" + }, + { + "input": "file:///C%7C/", + "base": "about:blank", + "href": "file:///C%7C/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C%7C/", + "search": "", + "hash": "" + }, + "# file URLs relative to other file URLs (via https://github.com/jsdom/whatwg-url/pull/60)", + { + "input": "pix/submit.gif", + "base": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/anchor.html", + "href": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///C:/", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# More file URL tests by zcorpan and annevk", + { + "input": "/", + "base": "file:///C:/a/b", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "//d:", + "base": "file:///C:/a/b", + "href": "file:///d:", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/d:", + "search": "", + "hash": "" + }, + { + "input": "//d:/..", + "base": "file:///C:/a/b", + "href": "file:///d:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/d:/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///ab:/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///1:/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "", + "base": "file:///test?test#test", + "href": "file:///test?test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "" + }, + { + "input": "file:", + "base": "file:///test?test#test", + "href": "file:///test?test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "" + }, + { + "input": "?x", + "base": "file:///test?test#test", + "href": "file:///test?x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?x", + "hash": "" + }, + { + "input": "file:?x", + "base": "file:///test?test#test", + "href": "file:///test?x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?x", + "hash": "" + }, + { + "input": "#x", + "base": "file:///test?test#test", + "href": "file:///test?test#x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "#x" + }, + { + "input": "file:#x", + "base": "file:///test?test#test", + "href": "file:///test?test#x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "#x" + }, + "# File URLs and many (back)slashes", + { + "input": "file:///localhost//cat", + "base": "about:blank", + "href": "file:///localhost//cat", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/localhost//cat", + "search": "", + "hash": "" + }, + { + "input": "\\//pig", + "base": "file://lion/", + "href": "file:///pig", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pig", + "search": "", + "hash": "" + }, + { + "input": "file://", + "base": "file://ape/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# Windows drive letter handling with the 'file:' base URL", + { + "input": "C|#", + "base": "file://host/dir/file", + "href": "file:///C:#", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|?", + "base": "file://host/dir/file", + "href": "file:///C:?", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|/", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C|\n/", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C|\\", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C", + "base": "file://host/dir/file", + "href": "file://host/dir/C", + "protocol": "file:", + "username": "", + "password": "", + "host": "host", + "hostname": "host", + "port": "", + "pathname": "/dir/C", + "search": "", + "hash": "" + }, + { + "input": "C|a", + "base": "file://host/dir/file", + "href": "file://host/dir/C|a", + "protocol": "file:", + "username": "", + "password": "", + "host": "host", + "hostname": "host", + "port": "", + "pathname": "/dir/C|a", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk in the file slash state", + { + "input": "/c:/foo/bar", + "base": "file://host/path", + "href": "file:///c:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:/foo/bar", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk (no host)", + { + "input": "file:/C|/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://C|/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk with not empty host", + { + "input": "file://example.net/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://1.2.3.4/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://[1::8]/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + "# file URLs without base URL by Rimas Misevičius", + { + "input": "file:", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:?q=v", + "base": "about:blank", + "href": "file:///?q=v", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "?q=v", + "hash": "" + }, + { + "input": "file:#frag", + "base": "about:blank", + "href": "file:///#frag", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "#frag" + }, + "# IPv6 tests", + { + "input": "http://[1:0::]", + "base": "http://example.net/", + "href": "http://[1::]/", + "origin": "http://[1::]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[1::]", + "hostname": "[1::]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[0:1:2:3:4:5:6:7:8]", + "base": "http://example.net/", + "failure": true + }, + { + "input": "https://[0::0::0]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:.0]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:0:]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:1:2:3:4:5:6:7.0.0.0.1]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:1.00.0.0.0]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:1.290.0.0.0]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:1.23.23]", + "base": "about:blank", + "failure": true + }, + "# Empty host", + { + "input": "http://?", + "base": "about:blank", + "failure": true + }, + { + "input": "http://#", + "base": "about:blank", + "failure": true + }, + "Port overflow (2^32 + 81)", + { + "input": "http://f:4294967377/c", + "base": "http://example.org/", + "failure": true + }, + "Port overflow (2^64 + 81)", + { + "input": "http://f:18446744073709551697/c", + "base": "http://example.org/", + "failure": true + }, + "Port overflow (2^128 + 81)", + { + "input": "http://f:340282366920938463463374607431768211537/c", + "base": "http://example.org/", + "failure": true + }, + "# Non-special-URL path tests", + { + "input": "///", + "base": "sc://x/", + "href": "sc:///", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "tftp://foobar.com/someconfig;mode=netascii", + "base": "about:blank", + "href": "tftp://foobar.com/someconfig;mode=netascii", + "origin": "null", + "protocol": "tftp:", + "username": "", + "password": "", + "host": "foobar.com", + "hostname": "foobar.com", + "port": "", + "pathname": "/someconfig;mode=netascii", + "search": "", + "hash": "" + }, + { + "input": "telnet://user:pass@foobar.com:23/", + "base": "about:blank", + "href": "telnet://user:pass@foobar.com:23/", + "origin": "null", + "protocol": "telnet:", + "username": "user", + "password": "pass", + "host": "foobar.com:23", + "hostname": "foobar.com", + "port": "23", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ut2004://10.10.10.10:7777/Index.ut2", + "base": "about:blank", + "href": "ut2004://10.10.10.10:7777/Index.ut2", + "origin": "null", + "protocol": "ut2004:", + "username": "", + "password": "", + "host": "10.10.10.10:7777", + "hostname": "10.10.10.10", + "port": "7777", + "pathname": "/Index.ut2", + "search": "", + "hash": "" + }, + { + "input": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz", + "base": "about:blank", + "href": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz", + "origin": "null", + "protocol": "redis:", + "username": "foo", + "password": "bar", + "host": "somehost:6379", + "hostname": "somehost", + "port": "6379", + "pathname": "/0", + "search": "?baz=bam&qux=baz", + "hash": "" + }, + { + "input": "rsync://foo@host:911/sup", + "base": "about:blank", + "href": "rsync://foo@host:911/sup", + "origin": "null", + "protocol": "rsync:", + "username": "foo", + "password": "", + "host": "host:911", + "hostname": "host", + "port": "911", + "pathname": "/sup", + "search": "", + "hash": "" + }, + { + "input": "git://github.com/foo/bar.git", + "base": "about:blank", + "href": "git://github.com/foo/bar.git", + "origin": "null", + "protocol": "git:", + "username": "", + "password": "", + "host": "github.com", + "hostname": "github.com", + "port": "", + "pathname": "/foo/bar.git", + "search": "", + "hash": "" + }, + { + "input": "irc://myserver.com:6999/channel?passwd", + "base": "about:blank", + "href": "irc://myserver.com:6999/channel?passwd", + "origin": "null", + "protocol": "irc:", + "username": "", + "password": "", + "host": "myserver.com:6999", + "hostname": "myserver.com", + "port": "6999", + "pathname": "/channel", + "search": "?passwd", + "hash": "" + }, + { + "input": "dns://fw.example.org:9999/foo.bar.org?type=TXT", + "base": "about:blank", + "href": "dns://fw.example.org:9999/foo.bar.org?type=TXT", + "origin": "null", + "protocol": "dns:", + "username": "", + "password": "", + "host": "fw.example.org:9999", + "hostname": "fw.example.org", + "port": "9999", + "pathname": "/foo.bar.org", + "search": "?type=TXT", + "hash": "" + }, + { + "input": "ldap://localhost:389/ou=People,o=JNDITutorial", + "base": "about:blank", + "href": "ldap://localhost:389/ou=People,o=JNDITutorial", + "origin": "null", + "protocol": "ldap:", + "username": "", + "password": "", + "host": "localhost:389", + "hostname": "localhost", + "port": "389", + "pathname": "/ou=People,o=JNDITutorial", + "search": "", + "hash": "" + }, + { + "input": "git+https://github.com/foo/bar", + "base": "about:blank", + "href": "git+https://github.com/foo/bar", + "origin": "null", + "protocol": "git+https:", + "username": "", + "password": "", + "host": "github.com", + "hostname": "github.com", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "urn:ietf:rfc:2648", + "base": "about:blank", + "href": "urn:ietf:rfc:2648", + "origin": "null", + "protocol": "urn:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "ietf:rfc:2648", + "search": "", + "hash": "" + }, + { + "input": "tag:joe@example.org,2001:foo/bar", + "base": "about:blank", + "href": "tag:joe@example.org,2001:foo/bar", + "origin": "null", + "protocol": "tag:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "joe@example.org,2001:foo/bar", + "search": "", + "hash": "" + }, + "# percent encoded hosts in non-special-URLs", + { + "input": "non-special://%E2%80%A0/", + "base": "about:blank", + "href": "non-special://%E2%80%A0/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "%E2%80%A0", + "hostname": "%E2%80%A0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://H%4fSt/path", + "base": "about:blank", + "href": "non-special://H%4fSt/path", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "H%4fSt", + "hostname": "H%4fSt", + "port": "", + "pathname": "/path", + "search": "", + "hash": "" + }, + "# IPv6 in non-special-URLs", + { + "input": "non-special://[1:2:0:0:5:0:0:0]/", + "base": "about:blank", + "href": "non-special://[1:2:0:0:5::]/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2:0:0:5::]", + "hostname": "[1:2:0:0:5::]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[1:2:0:0:0:0:0:3]/", + "base": "about:blank", + "href": "non-special://[1:2::3]/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2::3]", + "hostname": "[1:2::3]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[1:2::3]:80/", + "base": "about:blank", + "href": "non-special://[1:2::3]:80/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2::3]:80", + "hostname": "[1:2::3]", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[:80/", + "base": "about:blank", + "failure": true + }, + { + "input": "blob:https://example.com:443/", + "base": "about:blank", + "href": "blob:https://example.com:443/", + "protocol": "blob:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "https://example.com:443/", + "search": "", + "hash": "" + }, + { + "input": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf", + "base": "about:blank", + "href": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf", + "protocol": "blob:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "d3958f5c-0777-0845-9dcf-2cb28783acaf", + "search": "", + "hash": "" + }, + "Invalid IPv4 radix digits", + { + "input": "http://0177.0.0.0189", + "base": "about:blank", + "href": "http://0177.0.0.0189/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0177.0.0.0189", + "hostname": "0177.0.0.0189", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0x7f.0.0.0x7g", + "base": "about:blank", + "href": "http://0x7f.0.0.0x7g/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0x7f.0.0.0x7g", + "hostname": "0x7f.0.0.0x7g", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0X7F.0.0.0X7G", + "base": "about:blank", + "href": "http://0x7f.0.0.0x7g/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0x7f.0.0.0x7g", + "hostname": "0x7f.0.0.0x7g", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid IPv4 portion of IPv6 address", + { + "input": "http://[::127.0.0.0.1]", + "base": "about:blank", + "failure": true + }, + "Uncompressed IPv6 addresses with 0", + { + "input": "http://[0:1:0:1:0:1:0:1]", + "base": "about:blank", + "href": "http://[0:1:0:1:0:1:0:1]/", + "protocol": "http:", + "username": "", + "password": "", + "host": "[0:1:0:1:0:1:0:1]", + "hostname": "[0:1:0:1:0:1:0:1]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[1:0:1:0:1:0:1:0]", + "base": "about:blank", + "href": "http://[1:0:1:0:1:0:1:0]/", + "protocol": "http:", + "username": "", + "password": "", + "host": "[1:0:1:0:1:0:1:0]", + "hostname": "[1:0:1:0:1:0:1:0]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Percent-encoded query and fragment", + { + "input": "http://example.org/test?\u0022", + "base": "about:blank", + "href": "http://example.org/test?%22", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%22", + "hash": "" + }, + { + "input": "http://example.org/test?\u0023", + "base": "about:blank", + "href": "http://example.org/test?#", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "http://example.org/test?\u003C", + "base": "about:blank", + "href": "http://example.org/test?%3C", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%3C", + "hash": "" + }, + { + "input": "http://example.org/test?\u003E", + "base": "about:blank", + "href": "http://example.org/test?%3E", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%3E", + "hash": "" + }, + { + "input": "http://example.org/test?\u2323", + "base": "about:blank", + "href": "http://example.org/test?%E2%8C%A3", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%E2%8C%A3", + "hash": "" + }, + { + "input": "http://example.org/test?%23%23", + "base": "about:blank", + "href": "http://example.org/test?%23%23", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%23%23", + "hash": "" + }, + { + "input": "http://example.org/test?%GH", + "base": "about:blank", + "href": "http://example.org/test?%GH", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%GH", + "hash": "" + }, + { + "input": "http://example.org/test?a#%EF", + "base": "about:blank", + "href": "http://example.org/test?a#%EF", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#%EF" + }, + { + "input": "http://example.org/test?a#%GH", + "base": "about:blank", + "href": "http://example.org/test?a#%GH", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#%GH" + }, + "Bad bases", + { + "input": "test-a.html", + "base": "a", + "failure": true + }, + { + "input": "test-a-slash.html", + "base": "a/", + "failure": true + }, + { + "input": "test-a-slash-slash.html", + "base": "a//", + "failure": true + }, + { + "input": "test-a-colon.html", + "base": "a:", + "failure": true + }, + { + "input": "test-a-colon-slash.html", + "base": "a:/", + "href": "a:/test-a-colon-slash.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test-a-colon-slash.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-slash-slash.html", + "base": "a://", + "href": "a:///test-a-colon-slash-slash.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test-a-colon-slash-slash.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-b.html", + "base": "a:b", + "failure": true + }, + { + "input": "test-a-colon-slash-b.html", + "base": "a:/b", + "href": "a:/test-a-colon-slash-b.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test-a-colon-slash-b.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-slash-slash-b.html", + "base": "a://b", + "href": "a://b/test-a-colon-slash-slash-b.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "b", + "hostname": "b", + "port": "", + "pathname": "/test-a-colon-slash-slash-b.html", + "search": "", + "hash": "" + }, + "Null code point in fragment", + { + "input": "http://example.org/test?a#b\u0000c", + "base": "about:blank", + "href": "http://example.org/test?a#bc", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#bc" + } +] diff --git a/netwerk/test/gtest/urltestdata.json b/netwerk/test/gtest/urltestdata.json new file mode 100644 index 0000000000..b093656ff4 --- /dev/null +++ b/netwerk/test/gtest/urltestdata.json @@ -0,0 +1,6480 @@ +[ + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/segments.js", + { + "input": "http://example\t.\norg", + "base": "http://example.org/foo/bar", + "href": "http://example.org/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://user:pass@foo:21/bar;par?b#c", + "base": "http://example.org/foo/bar", + "href": "http://user:pass@foo:21/bar;par?b#c", + "origin": "http://foo:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "foo:21", + "hostname": "foo", + "port": "21", + "pathname": "/bar;par", + "search": "?b", + "hash": "#c" + }, + { + "input": "https://test:@test", + "base": "about:blank", + "href": "https://test@test/", + "origin": "https://test", + "protocol": "https:", + "username": "test", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://:@test", + "base": "about:blank", + "href": "https://test/", + "origin": "https://test", + "protocol": "https:", + "username": "", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://test:@test/x", + "base": "about:blank", + "href": "non-special://test@test/x", + "origin": "null", + "protocol": "non-special:", + "username": "test", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/x", + "search": "", + "hash": "" + }, + { + "input": "non-special://:@test/x", + "base": "about:blank", + "href": "non-special://test/x", + "origin": "null", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/x", + "search": "", + "hash": "" + }, + { + "input": "http:foo.com", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/foo.com", + "search": "", + "hash": "" + }, + { + "input": "\t :foo.com \n", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com", + "search": "", + "hash": "" + }, + { + "input": " foo.com ", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/foo.com", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/foo.com", + "search": "", + "hash": "" + }, + { + "input": "a:\t foo.com", + "base": "http://example.org/foo/bar", + "href": "a: foo.com", + "origin": "null", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": " foo.com", + "search": "", + "hash": "" + }, + { + "input": "http://f:21/ b ? d # e ", + "base": "http://example.org/foo/bar", + "href": "http://f:21/%20b%20?%20d%20# e", + "origin": "http://f:21", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:21", + "hostname": "f", + "port": "21", + "pathname": "/%20b%20", + "search": "?%20d%20", + "hash": "# e" + }, + { + "input": "lolscheme:x x#x x", + "base": "about:blank", + "href": "lolscheme:x x#x x", + "protocol": "lolscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "x x", + "search": "", + "hash": "#x x" + }, + { + "input": "http://f:/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:0/c", + "base": "http://example.org/foo/bar", + "href": "http://f:0/c", + "origin": "http://f:0", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:0", + "hostname": "f", + "port": "0", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:00000000000000/c", + "base": "http://example.org/foo/bar", + "href": "http://f:0/c", + "origin": "http://f:0", + "protocol": "http:", + "username": "", + "password": "", + "host": "f:0", + "hostname": "f", + "port": "0", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:00000000000000000000080/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:b/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f: /c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f:\n/c", + "base": "http://example.org/foo/bar", + "href": "http://f/c", + "origin": "http://f", + "protocol": "http:", + "username": "", + "password": "", + "host": "f", + "hostname": "f", + "port": "", + "pathname": "/c", + "search": "", + "hash": "" + }, + { + "input": "http://f:fifty-two/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f:999999/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "non-special://f:999999/c", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://f: 21 / b ? d # e ", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": " \t", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": ":foo.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com/", + "search": "", + "hash": "" + }, + { + "input": ":foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:foo.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:foo.com/", + "search": "", + "hash": "" + }, + { + "input": ":", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:", + "search": "", + "hash": "" + }, + { + "input": ":a", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:a", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:a", + "search": "", + "hash": "" + }, + { + "input": ":/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:/", + "search": "", + "hash": "" + }, + { + "input": ":\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:/", + "search": "", + "hash": "" + }, + { + "input": ":#", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:#", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:", + "search": "", + "hash": "" + }, + { + "input": "#", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "#/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#/" + }, + { + "input": "#\\", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#\\", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#\\" + }, + { + "input": "#;?", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#;?", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#;?" + }, + { + "input": "?", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar?", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": ":23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:23", + "search": "", + "hash": "" + }, + { + "input": "/:23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/:23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/:23", + "search": "", + "hash": "" + }, + { + "input": "::", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/::", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/::", + "search": "", + "hash": "" + }, + { + "input": "::23", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/::23", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/::23", + "search": "", + "hash": "" + }, + { + "input": "foo://", + "base": "http://example.org/foo/bar", + "href": "foo:///", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://a:b@c:29/d", + "base": "http://example.org/foo/bar", + "href": "http://a:b@c:29/d", + "origin": "http://c:29", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "c:29", + "hostname": "c", + "port": "29", + "pathname": "/d", + "search": "", + "hash": "" + }, + { + "input": "http::@c:29", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/:@c:29", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/:@c:29", + "search": "", + "hash": "" + }, + { + "input": "http://&a:foo(b]c@d:2/", + "base": "http://example.org/foo/bar", + "href": "http://&a:foo(b%5Dc@d:2/", + "origin": "http://d:2", + "protocol": "http:", + "username": "&a", + "password": "foo(b%5Dc", + "host": "d:2", + "hostname": "d", + "port": "2", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://::@c@d:2", + "base": "http://example.org/foo/bar", + "href": "http://:%3A%40c@d:2/", + "origin": "http://d:2", + "protocol": "http:", + "username": "", + "password": "%3A%40c", + "host": "d:2", + "hostname": "d", + "port": "2", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo.com:b@d/", + "base": "http://example.org/foo/bar", + "href": "http://foo.com:b@d/", + "origin": "http://d", + "protocol": "http:", + "username": "foo.com", + "password": "b", + "host": "d", + "hostname": "d", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo.com/\\@", + "base": "http://example.org/foo/bar", + "href": "http://foo.com//@", + "origin": "http://foo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.com", + "hostname": "foo.com", + "port": "", + "pathname": "//@", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://foo.com/", + "origin": "http://foo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.com", + "hostname": "foo.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\a\\b:c\\d@foo.com\\", + "base": "http://example.org/foo/bar", + "href": "http://a/b:c/d@foo.com/", + "origin": "http://a", + "protocol": "http:", + "username": "", + "password": "", + "host": "a", + "hostname": "a", + "port": "", + "pathname": "/b:c/d@foo.com/", + "search": "", + "hash": "" + }, + { + "input": "foo:/", + "base": "http://example.org/foo/bar", + "href": "foo:/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "foo:/bar.com/", + "base": "http://example.org/foo/bar", + "href": "foo:/bar.com/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/bar.com/", + "search": "", + "hash": "" + }, + { + "input": "foo://///////", + "base": "http://example.org/foo/bar", + "href": "foo://///////", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "///////", + "search": "", + "hash": "" + }, + { + "input": "foo://///////bar.com/", + "base": "http://example.org/foo/bar", + "href": "foo://///////bar.com/", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "///////bar.com/", + "search": "", + "hash": "" + }, + { + "input": "foo:////://///", + "base": "http://example.org/foo/bar", + "href": "foo:////://///", + "origin": "null", + "protocol": "foo:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "//://///", + "search": "", + "hash": "" + }, + { + "input": "c:/foo", + "base": "http://example.org/foo/bar", + "href": "c:/foo", + "origin": "null", + "protocol": "c:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "//foo/bar", + "base": "http://example.org/foo/bar", + "href": "http://foo/bar", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/bar", + "search": "", + "hash": "" + }, + { + "input": "http://foo/path;a??e#f#g", + "base": "http://example.org/foo/bar", + "href": "http://foo/path;a??e#f#g", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/path;a", + "search": "??e", + "hash": "#f#g" + }, + { + "input": "http://foo/abcd?efgh?ijkl", + "base": "http://example.org/foo/bar", + "href": "http://foo/abcd?efgh?ijkl", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/abcd", + "search": "?efgh?ijkl", + "hash": "" + }, + { + "input": "http://foo/abcd#foo?bar", + "base": "http://example.org/foo/bar", + "href": "http://foo/abcd#foo?bar", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/abcd", + "search": "", + "hash": "#foo?bar" + }, + { + "input": "[61:24:74]:98", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/[61:24:74]:98", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/[61:24:74]:98", + "search": "", + "hash": "" + }, + { + "input": "http:[61:27]/:foo", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/[61:27]/:foo", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/[61:27]/:foo", + "search": "", + "hash": "" + }, + { + "input": "http://[1::2]:3:4", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1]", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://2001::1]:80", + "base": "http://example.org/foo/bar", + "failure": true + }, + { + "input": "http://[2001::1]", + "base": "http://example.org/foo/bar", + "href": "http://[2001::1]/", + "origin": "http://[2001::1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[2001::1]", + "hostname": "[2001::1]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[::127.0.0.1]", + "base": "http://example.org/foo/bar", + "href": "http://[::7f00:1]/", + "origin": "http://[::7f00:1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[::7f00:1]", + "hostname": "[::7f00:1]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[0:0:0:0:0:0:13.1.68.3]", + "base": "http://example.org/foo/bar", + "href": "http://[::d01:4403]/", + "origin": "http://[::d01:4403]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[::d01:4403]", + "hostname": "[::d01:4403]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[2001::1]:80", + "base": "http://example.org/foo/bar", + "href": "http://[2001::1]/", + "origin": "http://[2001::1]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[2001::1]", + "hostname": "[2001::1]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/example.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/example.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftp:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https:/example.com/", + "base": "http://example.org/foo/bar", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "madeupscheme:/example.com/", + "base": "http://example.org/foo/bar", + "href": "madeupscheme:/example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file:/example.com/", + "base": "http://example.org/foo/bar", + "href": "file:///example.com/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file://example:1/", + "base": "about:blank", + "failure": true + }, + { + "input": "file://example:test/", + "base": "about:blank", + "failure": true + }, + { + "input": "file://example%/", + "base": "about:blank", + "failure": true + }, + { + "input": "file://[example]/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftps:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ftps:/example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:/example.com/", + "base": "http://example.org/foo/bar", + "href": "gopher://example.com/", + "origin": "null", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws:/example.com/", + "base": "http://example.org/foo/bar", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss:/example.com/", + "base": "http://example.org/foo/bar", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "NS_NewURI for given input and base fails", + { + "input": "data:/example.com/", + "base": "http://example.org/foo/bar", + "href": "data:/example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "javascript:/example.com/", + "base": "http://example.org/foo/bar", + "href": "javascript:/example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/example.com/", + "base": "http://example.org/foo/bar", + "href": "mailto:/example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "http:example.com/", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/example.com/", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftp:example.com/", + "base": "http://example.org/foo/bar", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https:example.com/", + "base": "http://example.org/foo/bar", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "madeupscheme:example.com/", + "base": "http://example.org/foo/bar", + "href": "madeupscheme:example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:example.com/", + "base": "http://example.org/foo/bar", + "href": "ftps:example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:example.com/", + "base": "http://example.org/foo/bar", + "href": "gopher://example.com/", + "origin": "null", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws:example.com/", + "base": "http://example.org/foo/bar", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss:example.com/", + "base": "http://example.org/foo/bar", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "NS_NewURI for given input and base fails", + { + "input": "data:example.com/", + "base": "http://example.org/foo/bar", + "href": "data:example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "javascript:example.com/", + "base": "http://example.org/foo/bar", + "href": "javascript:example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:example.com/", + "base": "http://example.org/foo/bar", + "href": "mailto:example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "/a/b/c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/b/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/b/c", + "search": "", + "hash": "" + }, + { + "input": "/a/ /c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/%20/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/%20/c", + "search": "", + "hash": "" + }, + { + "input": "/a%2fc", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a%2fc", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a%2fc", + "search": "", + "hash": "" + }, + { + "input": "/a/%2f/c", + "base": "http://example.org/foo/bar", + "href": "http://example.org/a/%2f/c", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/a/%2f/c", + "search": "", + "hash": "" + }, + { + "input": "#β", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar#%CE%B2", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "#%CE%B2" + }, + { + "input": "data:text/html,test#test", + "base": "http://example.org/foo/bar", + "href": "data:text/html,test#test", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "text/html,test", + "search": "", + "hash": "#test" + }, + { + "input": "tel:1234567890", + "base": "http://example.org/foo/bar", + "href": "tel:1234567890", + "origin": "null", + "protocol": "tel:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "1234567890", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/file.html", + { + "input": "file:c:\\foo\\bar.html", + "base": "file:///tmp/mock/path", + "href": "file:///c:/foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:/foo/bar.html", + "search": "", + "hash": "" + }, + { + "input": " File:c|////foo\\bar.html", + "base": "file:///tmp/mock/path", + "href": "file:///c:////foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:////foo/bar.html", + "search": "", + "hash": "" + }, + { + "input": "C|/foo/bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "/C|\\foo\\bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "//C|/foo/bar", + "base": "file:///tmp/mock/path", + "href": "file:///C:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "//server/file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "server", + "hostname": "server", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "\\\\server\\file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "server", + "hostname": "server", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "/\\server/file", + "base": "file:///tmp/mock/path", + "href": "file://server/file", + "protocol": "file:", + "username": "", + "password": "", + "host": "server", + "hostname": "server", + "port": "", + "pathname": "/file", + "search": "", + "hash": "" + }, + { + "input": "file:///foo/bar.txt", + "base": "file:///tmp/mock/path", + "href": "file:///foo/bar.txt", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/foo/bar.txt", + "search": "", + "hash": "" + }, + { + "input": "file:///home/me", + "base": "file:///tmp/mock/path", + "href": "file:///home/me", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/home/me", + "search": "", + "hash": "" + }, + { + "input": "//", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "///", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "///test", + "base": "file:///tmp/mock/path", + "href": "file:///test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "file://test", + "base": "file:///tmp/mock/path", + "href": "file://test/", + "protocol": "file:", + "username": "", + "password": "", + "host": "test", + "hostname": "test", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost/", + "base": "file:///tmp/mock/path", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file://localhost/test", + "base": "file:///tmp/mock/path", + "href": "file:///test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "test", + "base": "file:///tmp/mock/path", + "href": "file:///tmp/mock/test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/tmp/mock/test", + "search": "", + "hash": "" + }, + { + "input": "file:test", + "base": "file:///tmp/mock/path", + "href": "file:///tmp/mock/test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/tmp/mock/test", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/path.js", + { + "input": "http://example.com/././foo", + "base": "about:blank", + "href": "http://example.com/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/./.foo", + "base": "about:blank", + "href": "http://example.com/.foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/.foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/.", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/./", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/..", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/..bar", + "base": "about:blank", + "href": "http://example.com/foo/..bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/..bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../ton", + "base": "about:blank", + "href": "http://example.com/foo/ton", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/ton", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar/../ton/../../a", + "base": "about:blank", + "href": "http://example.com/a", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/a", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/../../..", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/../../../ton", + "base": "about:blank", + "href": "http://example.com/ton", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/ton", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e%2", + "base": "about:blank", + "href": "http://example.com/foo/%2e%2", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/%2e%2", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar", + "base": "about:blank", + "href": "http://example.com/%2e.bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%2e.bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com////../..", + "base": "about:blank", + "href": "http://example.com//", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "//", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar//../..", + "base": "about:blank", + "href": "http://example.com/foo/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo/bar//..", + "base": "about:blank", + "href": "http://example.com/foo/bar/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo/bar/", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo", + "base": "about:blank", + "href": "http://example.com/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%20foo", + "base": "about:blank", + "href": "http://example.com/%20foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%20foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%", + "base": "about:blank", + "href": "http://example.com/foo%", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%2", + "base": "about:blank", + "href": "http://example.com/foo%2", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%2zbar", + "base": "about:blank", + "href": "http://example.com/foo%2zbar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2zbar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%2©zbar", + "base": "about:blank", + "href": "http://example.com/foo%2%C3%82%C2%A9zbar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%2%C3%82%C2%A9zbar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%41%7a", + "base": "about:blank", + "href": "http://example.com/foo%41%7a", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%41%7a", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo\t\u0091%91", + "base": "about:blank", + "href": "http://example.com/foo%C2%91%91", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%C2%91%91", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo%00%51", + "base": "about:blank", + "href": "http://example.com/foo%00%51", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foo%00%51", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/(%28:%3A%29)", + "base": "about:blank", + "href": "http://example.com/(%28:%3A%29)", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/(%28:%3A%29)", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%3A%3a%3C%3c", + "base": "about:blank", + "href": "http://example.com/%3A%3a%3C%3c", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%3A%3a%3C%3c", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/foo\tbar", + "base": "about:blank", + "href": "http://example.com/foobar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/foobar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com\\\\foo\\\\bar", + "base": "about:blank", + "href": "http://example.com//foo//bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "//foo//bar", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd", + "base": "about:blank", + "href": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%7Ffp3%3Eju%3Dduvgw%3Dd", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/@asdf%40", + "base": "about:blank", + "href": "http://example.com/@asdf%40", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/@asdf%40", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/你好你好", + "base": "about:blank", + "href": "http://example.com/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD", + "search": "", + "hash": "" + }, + { + "input": "http://example.com/‥/foo", + "base": "about:blank", + "href": "http://example.com/%E2%80%A5/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E2%80%A5/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com//foo", + "base": "about:blank", + "href": "http://example.com/%EF%BB%BF/foo", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%EF%BB%BF/foo", + "search": "", + "hash": "" + }, + { + "input": "http://example.com//foo//bar", + "base": "about:blank", + "href": "http://example.com/%E2%80%AE/foo/%E2%80%AD/bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/%E2%80%AE/foo/%E2%80%AD/bar", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/relative.js", + { + "input": "http://www.google.com/foo?bar=baz#", + "base": "about:blank", + "href": "http://www.google.com/foo?bar=baz#", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "?bar=baz", + "hash": "" + }, + { + "input": "http://www.google.com/foo?bar=baz# »", + "base": "about:blank", + "href": "http://www.google.com/foo?bar=baz# %C2%BB", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "?bar=baz", + "hash": "# %C2%BB" + }, + "NS_NewURI for given input and base fails", + { + "input": "data:test# »", + "base": "about:blank", + "href": "data:test# %C2%BB", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "test", + "search": "", + "hash": "# %C2%BB", + "skip": true + }, + { + "input": "http://www.google.com", + "base": "about:blank", + "href": "http://www.google.com/", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.0x00A80001", + "base": "about:blank", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.168.0.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://www/foo%2Ehtml", + "base": "about:blank", + "href": "http://www/foo%2Ehtml", + "origin": "http://www", + "protocol": "http:", + "username": "", + "password": "", + "host": "www", + "hostname": "www", + "port": "", + "pathname": "/foo%2Ehtml", + "search": "", + "hash": "" + }, + { + "input": "http://www/foo/%2E/html", + "base": "about:blank", + "href": "http://www/foo/html", + "origin": "http://www", + "protocol": "http:", + "username": "", + "password": "", + "host": "www", + "hostname": "www", + "port": "", + "pathname": "/foo/html", + "search": "", + "hash": "" + }, + { + "input": "http://user:pass@/", + "base": "about:blank", + "failure": true + }, + { + "input": "http://%25DOMAIN:foobar@foodomain.com/", + "base": "about:blank", + "href": "http://%25DOMAIN:foobar@foodomain.com/", + "origin": "http://foodomain.com", + "protocol": "http:", + "username": "%25DOMAIN", + "password": "foobar", + "host": "foodomain.com", + "hostname": "foodomain.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:\\\\www.google.com\\foo", + "base": "about:blank", + "href": "http://www.google.com/foo", + "origin": "http://www.google.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.google.com", + "hostname": "www.google.com", + "port": "", + "pathname": "/foo", + "search": "", + "hash": "" + }, + { + "input": "http://foo:80/", + "base": "about:blank", + "href": "http://foo/", + "origin": "http://foo", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo:81/", + "base": "about:blank", + "href": "http://foo:81/", + "origin": "http://foo:81", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "httpa://foo:80/", + "base": "about:blank", + "href": "httpa://foo:80/", + "origin": "null", + "protocol": "httpa:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://foo:-80/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://foo:443/", + "base": "about:blank", + "href": "https://foo/", + "origin": "https://foo", + "protocol": "https:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://foo:80/", + "base": "about:blank", + "href": "https://foo:80/", + "origin": "https://foo:80", + "protocol": "https:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp://foo:21/", + "base": "about:blank", + "href": "ftp://foo/", + "origin": "ftp://foo", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp://foo:80/", + "base": "about:blank", + "href": "ftp://foo:80/", + "origin": "ftp://foo:80", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "gopher://foo:70/", + "base": "about:blank", + "href": "gopher://foo/", + "origin": "null", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "gopher://foo:443/", + "base": "about:blank", + "href": "gopher://foo:443/", + "origin": "null", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "foo:443", + "hostname": "foo", + "port": "443", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:80/", + "base": "about:blank", + "href": "ws://foo/", + "origin": "ws://foo", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:81/", + "base": "about:blank", + "href": "ws://foo:81/", + "origin": "ws://foo:81", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:443/", + "base": "about:blank", + "href": "ws://foo:443/", + "origin": "ws://foo:443", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:443", + "hostname": "foo", + "port": "443", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws://foo:815/", + "base": "about:blank", + "href": "ws://foo:815/", + "origin": "ws://foo:815", + "protocol": "ws:", + "username": "", + "password": "", + "host": "foo:815", + "hostname": "foo", + "port": "815", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:80/", + "base": "about:blank", + "href": "wss://foo:80/", + "origin": "wss://foo:80", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:80", + "hostname": "foo", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:81/", + "base": "about:blank", + "href": "wss://foo:81/", + "origin": "wss://foo:81", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:81", + "hostname": "foo", + "port": "81", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:443/", + "base": "about:blank", + "href": "wss://foo/", + "origin": "wss://foo", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo", + "hostname": "foo", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss://foo:815/", + "base": "about:blank", + "href": "wss://foo:815/", + "origin": "wss://foo:815", + "protocol": "wss:", + "username": "", + "password": "", + "host": "foo:815", + "hostname": "foo", + "port": "815", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/example.com/", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp:/example.com/", + "base": "about:blank", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https:/example.com/", + "base": "about:blank", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "madeupscheme:/example.com/", + "base": "about:blank", + "href": "madeupscheme:/example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "file:/example.com/", + "base": "about:blank", + "href": "file:///example.com/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:/example.com/", + "base": "about:blank", + "href": "ftps:/example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:/example.com/", + "base": "about:blank", + "href": "gopher://example.com/", + "origin": "null", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws:/example.com/", + "base": "about:blank", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss:/example.com/", + "base": "about:blank", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "NS_NewURI for given input and base fails", + { + "input": "data:/example.com/", + "base": "about:blank", + "href": "data:/example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "javascript:/example.com/", + "base": "about:blank", + "href": "javascript:/example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/example.com/", + "base": "about:blank", + "href": "mailto:/example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/example.com/", + "search": "", + "hash": "" + }, + { + "input": "http:example.com/", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ftp:example.com/", + "base": "about:blank", + "href": "ftp://example.com/", + "origin": "ftp://example.com", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https:example.com/", + "base": "about:blank", + "href": "https://example.com/", + "origin": "https://example.com", + "protocol": "https:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "madeupscheme:example.com/", + "base": "about:blank", + "href": "madeupscheme:example.com/", + "origin": "null", + "protocol": "madeupscheme:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "ftps:example.com/", + "base": "about:blank", + "href": "ftps:example.com/", + "origin": "null", + "protocol": "ftps:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "gopher:example.com/", + "base": "about:blank", + "href": "gopher://example.com/", + "origin": "null", + "protocol": "gopher:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ws:example.com/", + "base": "about:blank", + "href": "ws://example.com/", + "origin": "ws://example.com", + "protocol": "ws:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "wss:example.com/", + "base": "about:blank", + "href": "wss://example.com/", + "origin": "wss://example.com", + "protocol": "wss:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "NS_NewURI for given input and base fails", + { + "input": "data:example.com/", + "base": "about:blank", + "href": "data:example.com/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "javascript:example.com/", + "base": "about:blank", + "href": "javascript:example.com/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + { + "input": "mailto:example.com/", + "base": "about:blank", + "href": "mailto:example.com/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "example.com/", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/segments-userinfo-vs-host.html", + { + "input": "http:@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://a:b@www.example.com", + "base": "about:blank", + "href": "http://a:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://@pple.com", + "base": "about:blank", + "href": "http://pple.com/", + "origin": "http://pple.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "pple.com", + "hostname": "pple.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "NS_NewURI for given input and base fails", + { + "input": "http::b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "http:/:b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://:b@www.example.com", + "base": "about:blank", + "href": "http://:b@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "b", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/:@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://user@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:/@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "https:@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:a:b@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:/a:b@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://a:b@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http::@/www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:/a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://a:@www.example.com", + "base": "about:blank", + "href": "http://a@www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "a", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://www.@pple.com", + "base": "about:blank", + "href": "http://www.@pple.com/", + "origin": "http://pple.com", + "protocol": "http:", + "username": "www.", + "password": "", + "host": "pple.com", + "hostname": "pple.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http:@:www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http:/@:www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://@:www.example.com", + "base": "about:blank", + "failure": true + }, + { + "input": "http://:@www.example.com", + "base": "about:blank", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# Others", + { + "input": "/", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": ".", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "http://www.example.com/test", + "href": "http://www.example.com/", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "./test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../aaa/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/aaa/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/aaa/test.txt", + "search": "", + "hash": "" + }, + { + "input": "../../test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/test.txt", + "search": "", + "hash": "" + }, + { + "input": "中/test.txt", + "base": "http://www.example.com/test", + "href": "http://www.example.com/%E4%B8%AD/test.txt", + "origin": "http://www.example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example.com", + "hostname": "www.example.com", + "port": "", + "pathname": "/%E4%B8%AD/test.txt", + "search": "", + "hash": "" + }, + { + "input": "http://www.example2.com", + "base": "http://www.example.com/test", + "href": "http://www.example2.com/", + "origin": "http://www.example2.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example2.com", + "hostname": "www.example2.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "//www.example2.com", + "base": "http://www.example.com/test", + "href": "http://www.example2.com/", + "origin": "http://www.example2.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.example2.com", + "hostname": "www.example2.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:...", + "base": "http://www.example.com/test", + "href": "file:///...", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/...", + "search": "", + "hash": "" + }, + { + "input": "file:..", + "base": "http://www.example.com/test", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:a", + "base": "http://www.example.com/test", + "href": "file:///a", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/a", + "search": "", + "hash": "" + }, + "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/host.html", + "Basic canonicalization, uppercase should be converted to lowercase", + { + "input": "http://ExAmPlE.CoM", + "base": "http://other.com/", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://example example.com", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://Goo%20 goo%7C|.com", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[:]", + "base": "http://other.com/", + "failure": true + }, + "U+3000 is mapped to U+0020 (space) which is disallowed", + { + "input": "http://GOO\u00a0\u3000goo.com", + "base": "http://other.com/", + "failure": true + }, + "Other types of space (no-break, zero-width, zero-width-no-break) are name-prepped away to nothing. U+200B, U+2060, and U+FEFF, are ignored", + { + "input": "http://GOO\u200b\u2060\ufeffgoo.com", + "base": "http://other.com/", + "href": "http://googoo.com/", + "origin": "http://googoo.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "googoo.com", + "hostname": "googoo.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Leading and trailing C0 control or space", + { + "input": "\u0000\u001b\u0004\u0012 http://example.com/\u001f \u000d ", + "base": "about:blank", + "href": "http://example.com/", + "origin": "http://example.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Ideographic full stop (full-width period for Chinese, etc.) should be treated as a dot. U+3002 is mapped to U+002E (dot)", + { + "input": "http://www.foo。bar.com", + "base": "http://other.com/", + "href": "http://www.foo.bar.com/", + "origin": "http://www.foo.bar.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "www.foo.bar.com", + "hostname": "www.foo.bar.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid unicode characters should fail... U+FDD0 is disallowed; %ef%b7%90 is U+FDD0", + { + "input": "http://\ufdd0zyx.com", + "base": "http://other.com/", + "failure": true + }, + "This is the same as previous but escaped", + { + "input": "http://%ef%b7%90zyx.com", + "base": "http://other.com/", + "failure": true + }, + "U+FFFD", + { + "input": "https://\ufffd", + "base": "about:blank", + "failure": true + }, + { + "input": "https://%EF%BF%BD", + "base": "about:blank", + "failure": true + }, + { + "input": "https://x/\ufffd?\ufffd#\ufffd", + "base": "about:blank", + "href": "https://x/%EF%BF%BD?%EF%BF%BD#%EF%BF%BD", + "origin": "https://x", + "protocol": "https:", + "username": "", + "password": "", + "host": "x", + "hostname": "x", + "port": "", + "pathname": "/%EF%BF%BD", + "search": "?%EF%BF%BD", + "hash": "#%EF%BF%BD" + }, + "Test name prepping, fullwidth input should be converted to ASCII and NOT IDN-ized. This is 'Go' in fullwidth UTF-8/UTF-16.", + { + "input": "http://Go.com", + "base": "http://other.com/", + "href": "http://go.com/", + "origin": "http://go.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "go.com", + "hostname": "go.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "URL spec forbids the following. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24257", + { + "input": "http://%41.com", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://%ef%bc%85%ef%bc%94%ef%bc%91.com", + "base": "http://other.com/", + "failure": true + }, + "...%00 in fullwidth should fail (also as escaped UTF-8 input)", + { + "input": "http://%00.com", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://%ef%bc%85%ef%bc%90%ef%bc%90.com", + "base": "http://other.com/", + "failure": true + }, + "Basic IDN support, UTF-8 and UTF-16 input should be converted to IDN", + { + "input": "http://你好你好", + "base": "http://other.com/", + "href": "http://xn--6qqa088eba/", + "origin": "http://xn--6qqa088eba", + "protocol": "http:", + "username": "", + "password": "", + "host": "xn--6qqa088eba", + "hostname": "xn--6qqa088eba", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Origin computed by MozURL is wrong", + { + "input": "https://faß.ExAmPlE/", + "base": "about:blank", + "href": "https://xn--fa-hia.example/", + "origin": "https://xn--fa-hia.example", + "protocol": "https:", + "username": "", + "password": "", + "host": "xn--fa-hia.example", + "hostname": "xn--fa-hia.example", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "sc://faß.ExAmPlE/", + "base": "about:blank", + "href": "sc://fa%C3%9F.ExAmPlE/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "fa%C3%9F.ExAmPlE", + "hostname": "fa%C3%9F.ExAmPlE", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid escaped characters should fail and the percents should be escaped. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24191", + { + "input": "http://%zz%66%a.com", + "base": "http://other.com/", + "failure": true + }, + "If we get an invalid character that has been escaped.", + { + "input": "http://%25", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://hello%00", + "base": "http://other.com/", + "failure": true + }, + "Escaped numbers should be treated like IP addresses if they are.", + { + "input": "http://%30%78%63%30%2e%30%32%35%30.01", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.168.0.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://%30%78%63%30%2e%30%32%35%30.01%2e", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.168.0.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.168.0.257", + "base": "http://other.com/", + "failure": true + }, + "Invalid escaping in hosts causes failure", + { + "input": "http://%3g%78%63%30%2e%30%32%35%30%2E.01", + "base": "http://other.com/", + "failure": true + }, + "A space in a host causes failure", + { + "input": "http://192.168.0.1 hello", + "base": "http://other.com/", + "failure": true + }, + { + "input": "https://x x:12", + "base": "about:blank", + "failure": true + }, + "Fullwidth and escaped UTF-8 fullwidth should still be treated as IP", + { + "input": "http://0Xc0.0250.01", + "base": "http://other.com/", + "href": "http://192.168.0.1/", + "origin": "http://192.168.0.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.0.1", + "hostname": "192.168.0.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Domains with empty labels", + { + "input": "http://./", + "base": "about:blank", + "href": "http://./", + "origin": "http://.", + "protocol": "http:", + "username": "", + "password": "", + "host": ".", + "hostname": ".", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://../", + "base": "about:blank", + "href": "http://../", + "origin": "http://..", + "protocol": "http:", + "username": "", + "password": "", + "host": "..", + "hostname": "..", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0..0x300/", + "base": "about:blank", + "href": "http://0..0x300/", + "origin": "http://0..0x300", + "protocol": "http:", + "username": "", + "password": "", + "host": "0..0x300", + "hostname": "0..0x300", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Broken IPv6", + { + "input": "http://[www.google.com]/", + "base": "about:blank", + "failure": true + }, + { + "input": "http://[google.com]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[::1.2.3.4x]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[::1.2.3.]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[::1.2.]", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://[::1.]", + "base": "http://other.com/", + "failure": true + }, + "Misc Unicode", + { + "input": "http://foo:💩@example.com/bar", + "base": "http://other.com/", + "href": "http://foo:%F0%9F%92%A9@example.com/bar", + "origin": "http://example.com", + "protocol": "http:", + "username": "foo", + "password": "%F0%9F%92%A9", + "host": "example.com", + "hostname": "example.com", + "port": "", + "pathname": "/bar", + "search": "", + "hash": "" + }, + "# resolving a fragment against any scheme succeeds", + { + "input": "#", + "base": "test:test", + "href": "test:test#", + "origin": "null", + "protocol": "test:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "test", + "search": "", + "hash": "" + }, + { + "input": "#x", + "base": "mailto:x@x.com", + "href": "mailto:x@x.com#x", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "x@x.com", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "data:,", + "href": "data:,#x", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": ",", + "search": "", + "hash": "#x" + }, + { + "input": "#x", + "base": "about:blank", + "href": "about:blank#x", + "origin": "null", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "#x" + }, + { + "input": "#", + "base": "test:test?test", + "href": "test:test?test#", + "origin": "null", + "protocol": "test:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "test", + "search": "?test", + "hash": "" + }, + "# multiple @ in authority state", + { + "input": "https://@test@test@example:800/", + "base": "http://doesnotmatter/", + "href": "https://%40test%40test@example:800/", + "origin": "https://example:800", + "protocol": "https:", + "username": "%40test%40test", + "password": "", + "host": "example:800", + "hostname": "example", + "port": "800", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://@@@example", + "base": "http://doesnotmatter/", + "href": "https://%40%40@example/", + "origin": "https://example", + "protocol": "https:", + "username": "%40%40", + "password": "", + "host": "example", + "hostname": "example", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "non-az-09 characters", + { + "input": "http://`{}:`{}@h/`{}?`{}", + "base": "http://doesnotmatter/", + "href": "http://%60%7B%7D:%60%7B%7D@h/%60%7B%7D?`{}", + "origin": "http://h", + "protocol": "http:", + "username": "%60%7B%7D", + "password": "%60%7B%7D", + "host": "h", + "hostname": "h", + "port": "", + "pathname": "/%60%7B%7D", + "search": "?`{}", + "hash": "" + }, + "# Credentials in base", + { + "input": "/some/path", + "base": "http://user@example.org/smth", + "href": "http://user@example.org/some/path", + "origin": "http://example.org", + "protocol": "http:", + "username": "user", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/some/path", + "search": "", + "hash": "" + }, + { + "input": "", + "base": "http://user:pass@example.org:21/smth", + "href": "http://user:pass@example.org:21/smth", + "origin": "http://example.org:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "example.org:21", + "hostname": "example.org", + "port": "21", + "pathname": "/smth", + "search": "", + "hash": "" + }, + { + "input": "/some/path", + "base": "http://user:pass@example.org:21/smth", + "href": "http://user:pass@example.org:21/some/path", + "origin": "http://example.org:21", + "protocol": "http:", + "username": "user", + "password": "pass", + "host": "example.org:21", + "hostname": "example.org", + "port": "21", + "pathname": "/some/path", + "search": "", + "hash": "" + }, + "# a set of tests designed by zcorpan for relative URLs with unknown schemes", + { + "input": "i", + "base": "sc:sd", + "failure": true + }, + { + "input": "i", + "base": "sc:sd/sd", + "failure": true + }, + "NS_NewURI for given input and base fails", + { + "input": "i", + "base": "sc:/pa/pa", + "href": "sc:/pa/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/i", + "search": "", + "hash": "", + "skip": true + }, + "NS_NewURI for given input and base fails", + { + "input": "i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/i", + "search": "", + "hash": "", + "skip": true + }, + "NS_NewURI for given input and base fails", + { + "input": "i", + "base": "sc:///pa/pa", + "href": "sc:///pa/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/i", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "../i", + "base": "sc:sd", + "failure": true + }, + { + "input": "../i", + "base": "sc:sd/sd", + "failure": true + }, + "NS_NewURI for given input and base fails", + { + "input": "../i", + "base": "sc:/pa/pa", + "href": "sc:/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/i", + "search": "", + "hash": "", + "skip": true + }, + "NS_NewURI for given input and base fails", + { + "input": "../i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/i", + "search": "", + "hash": "", + "skip": true + }, + "NS_NewURI for given input and base fails", + { + "input": "../i", + "base": "sc:///pa/pa", + "href": "sc:///i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/i", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "/i", + "base": "sc:sd", + "failure": true + }, + { + "input": "/i", + "base": "sc:sd/sd", + "failure": true + }, + "NS_NewURI for given input and base fails", + { + "input": "/i", + "base": "sc:/pa/pa", + "href": "sc:/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/i", + "search": "", + "hash": "", + "skip": true + }, + "NS_NewURI for given input and base fails", + { + "input": "/i", + "base": "sc://ho/pa", + "href": "sc://ho/i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/i", + "search": "", + "hash": "", + "skip": true + }, + "NS_NewURI for given input and base fails", + { + "input": "/i", + "base": "sc:///pa/pa", + "href": "sc:///i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/i", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "?i", + "base": "sc:sd", + "failure": true + }, + { + "input": "?i", + "base": "sc:sd/sd", + "failure": true + }, + "NS_NewURI for given input and base fails", + { + "input": "?i", + "base": "sc:/pa/pa", + "href": "sc:/pa/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/pa", + "search": "?i", + "hash": "", + "skip": true + }, + "NS_NewURI for given input and base fails", + { + "input": "?i", + "base": "sc://ho/pa", + "href": "sc://ho/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/pa", + "search": "?i", + "hash": "", + "skip": true + }, + "NS_NewURI for given input and base fails", + { + "skip": true, + "input": "?i", + "base": "sc:///pa/pa", + "href": "sc:///pa/pa?i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/pa", + "search": "?i", + "hash": "" + }, + { + "input": "#i", + "base": "sc:sd", + "href": "sc:sd#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "sd", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:sd/sd", + "href": "sc:sd/sd#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "sd/sd", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:/pa/pa", + "href": "sc:/pa/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/pa", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc://ho/pa", + "href": "sc://ho/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "ho", + "hostname": "ho", + "port": "", + "pathname": "/pa", + "search": "", + "hash": "#i" + }, + { + "input": "#i", + "base": "sc:///pa/pa", + "href": "sc:///pa/pa#i", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pa/pa", + "search": "", + "hash": "#i" + }, + "# make sure that relative URL logic works on known typically non-relative schemes too", + "Origin computed by MozURL is wrong", + { + "input": "about:/../", + "base": "about:blank", + "href": "about:/", + "origin": "about:/../", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + "NS_NewURI for given input and base fails", + { + "input": "data:/../", + "base": "about:blank", + "href": "data:/", + "origin": "null", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + { + "input": "javascript:/../", + "base": "about:blank", + "href": "javascript:/", + "origin": "null", + "protocol": "javascript:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "mailto:/../", + "base": "about:blank", + "href": "mailto:/", + "origin": "null", + "protocol": "mailto:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# unknown schemes and their hosts", + { + "input": "sc://ñ.test/", + "base": "about:blank", + "href": "sc://%C3%B1.test/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1.test", + "hostname": "%C3%B1.test", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://\u001F!\"$&'()*+,-.;<=>^_`{|}~/", + "base": "about:blank", + "href": "sc://%1F!\"$&'()*+,-.;<=>^_`{|}~/", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%1F!\"$&'()*+,-.;<=>^_`{|}~", + "hostname": "%1F!\"$&'()*+,-.;<=>^_`{|}~", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://\u0000/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc:// /", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://%/", + "base": "about:blank", + "href": "sc://%/", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%", + "hostname": "%", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "sc://[/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://\\/", + "base": "about:blank", + "failure": true + }, + { + "input": "sc://]/", + "base": "about:blank", + "failure": true + }, + "NS_NewURI for given input and base fails", + { + "input": "x", + "base": "sc://ñ", + "href": "sc://%C3%B1/x", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "%C3%B1", + "hostname": "%C3%B1", + "port": "", + "pathname": "/x", + "search": "", + "hash": "", + "skip": true + }, + "# unknown schemes and backslashes", + { + "input": "sc:\\../", + "base": "about:blank", + "href": "sc:\\../", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "\\../", + "search": "", + "hash": "" + }, + "# unknown scheme with path looking like a password", + { + "input": "sc::a@example.net", + "base": "about:blank", + "href": "sc::a@example.net", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": ":a@example.net", + "search": "", + "hash": "" + }, + "# unknown scheme with bogus percent-encoding", + { + "input": "wow:%NBD", + "base": "about:blank", + "href": "wow:%NBD", + "origin": "null", + "protocol": "wow:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "%NBD", + "search": "", + "hash": "" + }, + { + "input": "wow:%1G", + "base": "about:blank", + "href": "wow:%1G", + "origin": "null", + "protocol": "wow:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "%1G", + "search": "", + "hash": "" + }, + "# Hosts and percent-encoding", + { + "input": "ftp://example.com%80/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftp://example.com%A0/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://example.com%80/", + "base": "about:blank", + "failure": true + }, + { + "input": "https://example.com%A0/", + "base": "about:blank", + "failure": true + }, + { + "input": "ftp://%e2%98%83", + "base": "about:blank", + "href": "ftp://xn--n3h/", + "origin": "ftp://xn--n3h", + "protocol": "ftp:", + "username": "", + "password": "", + "host": "xn--n3h", + "hostname": "xn--n3h", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "https://%e2%98%83", + "base": "about:blank", + "href": "https://xn--n3h/", + "origin": "https://xn--n3h", + "protocol": "https:", + "username": "", + "password": "", + "host": "xn--n3h", + "hostname": "xn--n3h", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# tests from jsdom/whatwg-url designed for code coverage", + { + "input": "http://127.0.0.1:10100/relative_import.html", + "base": "about:blank", + "href": "http://127.0.0.1:10100/relative_import.html", + "origin": "http://127.0.0.1:10100", + "protocol": "http:", + "username": "", + "password": "", + "host": "127.0.0.1:10100", + "hostname": "127.0.0.1", + "port": "10100", + "pathname": "/relative_import.html", + "search": "", + "hash": "" + }, + { + "input": "http://facebook.com/?foo=%7B%22abc%22", + "base": "about:blank", + "href": "http://facebook.com/?foo=%7B%22abc%22", + "origin": "http://facebook.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "facebook.com", + "hostname": "facebook.com", + "port": "", + "pathname": "/", + "search": "?foo=%7B%22abc%22", + "hash": "" + }, + { + "input": "https://localhost:3000/jqueryui@1.2.3", + "base": "about:blank", + "href": "https://localhost:3000/jqueryui@1.2.3", + "origin": "https://localhost:3000", + "protocol": "https:", + "username": "", + "password": "", + "host": "localhost:3000", + "hostname": "localhost", + "port": "3000", + "pathname": "/jqueryui@1.2.3", + "search": "", + "hash": "" + }, + "# tab/LF/CR", + { + "input": "h\tt\nt\rp://h\to\ns\rt:9\t0\n0\r0/p\ta\nt\rh?q\tu\ne\rry#f\tr\na\rg", + "base": "about:blank", + "href": "http://host:9000/path?query#frag", + "origin": "http://host:9000", + "protocol": "http:", + "username": "", + "password": "", + "host": "host:9000", + "hostname": "host", + "port": "9000", + "pathname": "/path", + "search": "?query", + "hash": "#frag" + }, + "# Stringification of URL.searchParams", + { + "input": "?a=b&c=d", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar?a=b&c=d", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "?a=b&c=d", + "searchParams": "a=b&c=d", + "hash": "" + }, + { + "input": "??a=b&c=d", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar??a=b&c=d", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "??a=b&c=d", + "searchParams": "%3Fa=b&c=d", + "hash": "" + }, + "# Scheme only", + { + "input": "http:", + "base": "http://example.org/foo/bar", + "href": "http://example.org/foo/bar", + "origin": "http://example.org", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/foo/bar", + "search": "", + "searchParams": "", + "hash": "" + }, + { + "input": "http:", + "base": "https://example.org/foo/bar", + "failure": true + }, + { + "input": "sc:", + "base": "https://example.org/foo/bar", + "href": "sc:", + "origin": "null", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "", + "search": "", + "searchParams": "", + "hash": "" + }, + "# Percent encoding of fragments", + { + "input": "http://foo.bar/baz?qux#foo\bbar", + "base": "about:blank", + "href": "http://foo.bar/baz?qux#foo%08bar", + "origin": "http://foo.bar", + "protocol": "http:", + "username": "", + "password": "", + "host": "foo.bar", + "hostname": "foo.bar", + "port": "", + "pathname": "/baz", + "search": "?qux", + "searchParams": "qux=", + "hash": "#foo%08bar" + }, + "# IPv4 parsing (via https://github.com/nodejs/node/pull/10317)", + { + "input": "http://192.168.257", + "base": "http://other.com/", + "href": "http://192.168.1.1/", + "origin": "http://192.168.1.1", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.1.1", + "hostname": "192.168.1.1", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://192.168.257.com", + "base": "http://other.com/", + "href": "http://192.168.257.com/", + "origin": "http://192.168.257.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "192.168.257.com", + "hostname": "192.168.257.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://256", + "base": "http://other.com/", + "href": "http://0.0.1.0/", + "origin": "http://0.0.1.0", + "protocol": "http:", + "username": "", + "password": "", + "host": "0.0.1.0", + "hostname": "0.0.1.0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://256.com", + "base": "http://other.com/", + "href": "http://256.com/", + "origin": "http://256.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "256.com", + "hostname": "256.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://999999999", + "base": "http://other.com/", + "href": "http://59.154.201.255/", + "origin": "http://59.154.201.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "59.154.201.255", + "hostname": "59.154.201.255", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://999999999.com", + "base": "http://other.com/", + "href": "http://999999999.com/", + "origin": "http://999999999.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "999999999.com", + "hostname": "999999999.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://10000000000", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://10000000000.com", + "base": "http://other.com/", + "href": "http://10000000000.com/", + "origin": "http://10000000000.com", + "protocol": "http:", + "username": "", + "password": "", + "host": "10000000000.com", + "hostname": "10000000000.com", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://4294967295", + "base": "http://other.com/", + "href": "http://255.255.255.255/", + "origin": "http://255.255.255.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "255.255.255.255", + "hostname": "255.255.255.255", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://4294967296", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://0xffffffff", + "base": "http://other.com/", + "href": "http://255.255.255.255/", + "origin": "http://255.255.255.255", + "protocol": "http:", + "username": "", + "password": "", + "host": "255.255.255.255", + "hostname": "255.255.255.255", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0xffffffff1", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://256.256.256.256", + "base": "http://other.com/", + "failure": true + }, + { + "input": "http://256.256.256.256.256", + "base": "http://other.com/", + "href": "http://256.256.256.256.256/", + "origin": "http://256.256.256.256.256", + "protocol": "http:", + "username": "", + "password": "", + "host": "256.256.256.256.256", + "hostname": "256.256.256.256.256", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Origin computed by MozURL is wrong", + { + "input": "https://0x.0x.0", + "base": "about:blank", + "href": "https://0.0.0.0/", + "origin": "https://0x.0x.0", + "protocol": "https:", + "username": "", + "password": "", + "host": "0.0.0.0", + "hostname": "0.0.0.0", + "port": "", + "pathname": "/", + "search": "", + "hash": "", + "skip": true + }, + "More IPv4 parsing (via https://github.com/jsdom/whatwg-url/issues/92)", + { + "input": "https://256.0.0.1/test", + "base": "about:blank", + "failure": true + }, + "# file URLs containing percent-encoded Windows drive letters (shouldn't work)", + { + "input": "file:///C%3A/", + "base": "about:blank", + "href": "file:///C%3A/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C%3A/", + "search": "", + "hash": "" + }, + { + "input": "file:///C%7C/", + "base": "about:blank", + "href": "file:///C%7C/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C%7C/", + "search": "", + "hash": "" + }, + "# file URLs relative to other file URLs (via https://github.com/jsdom/whatwg-url/pull/60)", + { + "input": "pix/submit.gif", + "base": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/anchor.html", + "href": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///C:/", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# More file URL tests by zcorpan and annevk", + { + "input": "/", + "base": "file:///C:/a/b", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "//d:", + "base": "file:///C:/a/b", + "href": "file:///d:", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/d:", + "search": "", + "hash": "" + }, + { + "input": "//d:/..", + "base": "file:///C:/a/b", + "href": "file:///d:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/d:/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///ab:/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "..", + "base": "file:///1:/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "", + "base": "file:///test?test#test", + "href": "file:///test?test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "" + }, + { + "input": "file:", + "base": "file:///test?test#test", + "href": "file:///test?test", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "" + }, + { + "input": "?x", + "base": "file:///test?test#test", + "href": "file:///test?x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?x", + "hash": "" + }, + { + "input": "file:?x", + "base": "file:///test?test#test", + "href": "file:///test?x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?x", + "hash": "" + }, + { + "input": "#x", + "base": "file:///test?test#test", + "href": "file:///test?test#x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "#x" + }, + { + "input": "file:#x", + "base": "file:///test?test#test", + "href": "file:///test?test#x", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test", + "search": "?test", + "hash": "#x" + }, + "# File URLs and many (back)slashes", + { + "input": "file:///localhost//cat", + "base": "about:blank", + "href": "file:///localhost//cat", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/localhost//cat", + "search": "", + "hash": "" + }, + { + "input": "\\//pig", + "base": "file://lion/", + "href": "file:///pig", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/pig", + "search": "", + "hash": "" + }, + { + "input": "file://", + "base": "file://ape/", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "# Windows drive letter handling with the 'file:' base URL", + { + "input": "C|#", + "base": "file://host/dir/file", + "href": "file:///C:#", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|?", + "base": "file://host/dir/file", + "href": "file:///C:?", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:", + "search": "", + "hash": "" + }, + { + "input": "C|/", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C|\n/", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C|\\", + "base": "file://host/dir/file", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "C", + "base": "file://host/dir/file", + "href": "file://host/dir/C", + "protocol": "file:", + "username": "", + "password": "", + "host": "host", + "hostname": "host", + "port": "", + "pathname": "/dir/C", + "search": "", + "hash": "" + }, + { + "input": "C|a", + "base": "file://host/dir/file", + "href": "file://host/dir/C|a", + "protocol": "file:", + "username": "", + "password": "", + "host": "host", + "hostname": "host", + "port": "", + "pathname": "/dir/C|a", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk in the file slash state", + { + "input": "/c:/foo/bar", + "base": "file://host/path", + "href": "file:///c:/foo/bar", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/c:/foo/bar", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk (no host)", + { + "input": "file:/C|/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://C|/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + "# Windows drive letter quirk with not empty host", + { + "input": "file://example.net/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://1.2.3.4/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + { + "input": "file://[1::8]/C:/", + "base": "about:blank", + "href": "file:///C:/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/", + "search": "", + "hash": "" + }, + "# file URLs without base URL by Rimas Misevičius", + { + "input": "file:", + "base": "about:blank", + "href": "file:///", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:?q=v", + "base": "about:blank", + "href": "file:///?q=v", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "?q=v", + "hash": "" + }, + { + "input": "file:#frag", + "base": "about:blank", + "href": "file:///#frag", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "#frag" + }, + "# IPv6 tests", + { + "input": "http://[1:0::]", + "base": "http://example.net/", + "href": "http://[1::]/", + "origin": "http://[1::]", + "protocol": "http:", + "username": "", + "password": "", + "host": "[1::]", + "hostname": "[1::]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[0:1:2:3:4:5:6:7:8]", + "base": "http://example.net/", + "failure": true + }, + { + "input": "https://[0::0::0]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:.0]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:0:]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:1:2:3:4:5:6:7.0.0.0.1]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:1.00.0.0.0]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:1.290.0.0.0]", + "base": "about:blank", + "failure": true + }, + { + "input": "https://[0:1.23.23]", + "base": "about:blank", + "failure": true + }, + "# Empty host", + { + "input": "http://?", + "base": "about:blank", + "failure": true + }, + { + "input": "http://#", + "base": "about:blank", + "failure": true + }, + "Port overflow (2^32 + 81)", + { + "input": "http://f:4294967377/c", + "base": "http://example.org/", + "failure": true + }, + "Port overflow (2^64 + 81)", + { + "input": "http://f:18446744073709551697/c", + "base": "http://example.org/", + "failure": true + }, + "Port overflow (2^128 + 81)", + { + "input": "http://f:340282366920938463463374607431768211537/c", + "base": "http://example.org/", + "failure": true + }, + "# Non-special-URL path tests", + { + "input": "///", + "base": "sc://x/", + "href": "sc:///", + "protocol": "sc:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "tftp://foobar.com/someconfig;mode=netascii", + "base": "about:blank", + "href": "tftp://foobar.com/someconfig;mode=netascii", + "origin": "null", + "protocol": "tftp:", + "username": "", + "password": "", + "host": "foobar.com", + "hostname": "foobar.com", + "port": "", + "pathname": "/someconfig;mode=netascii", + "search": "", + "hash": "" + }, + { + "input": "telnet://user:pass@foobar.com:23/", + "base": "about:blank", + "href": "telnet://user:pass@foobar.com:23/", + "origin": "null", + "protocol": "telnet:", + "username": "user", + "password": "pass", + "host": "foobar.com:23", + "hostname": "foobar.com", + "port": "23", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "ut2004://10.10.10.10:7777/Index.ut2", + "base": "about:blank", + "href": "ut2004://10.10.10.10:7777/Index.ut2", + "origin": "null", + "protocol": "ut2004:", + "username": "", + "password": "", + "host": "10.10.10.10:7777", + "hostname": "10.10.10.10", + "port": "7777", + "pathname": "/Index.ut2", + "search": "", + "hash": "" + }, + { + "input": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz", + "base": "about:blank", + "href": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz", + "origin": "null", + "protocol": "redis:", + "username": "foo", + "password": "bar", + "host": "somehost:6379", + "hostname": "somehost", + "port": "6379", + "pathname": "/0", + "search": "?baz=bam&qux=baz", + "hash": "" + }, + { + "input": "rsync://foo@host:911/sup", + "base": "about:blank", + "href": "rsync://foo@host:911/sup", + "origin": "null", + "protocol": "rsync:", + "username": "foo", + "password": "", + "host": "host:911", + "hostname": "host", + "port": "911", + "pathname": "/sup", + "search": "", + "hash": "" + }, + { + "input": "git://github.com/foo/bar.git", + "base": "about:blank", + "href": "git://github.com/foo/bar.git", + "origin": "null", + "protocol": "git:", + "username": "", + "password": "", + "host": "github.com", + "hostname": "github.com", + "port": "", + "pathname": "/foo/bar.git", + "search": "", + "hash": "" + }, + { + "input": "irc://myserver.com:6999/channel?passwd", + "base": "about:blank", + "href": "irc://myserver.com:6999/channel?passwd", + "origin": "null", + "protocol": "irc:", + "username": "", + "password": "", + "host": "myserver.com:6999", + "hostname": "myserver.com", + "port": "6999", + "pathname": "/channel", + "search": "?passwd", + "hash": "" + }, + { + "input": "dns://fw.example.org:9999/foo.bar.org?type=TXT", + "base": "about:blank", + "href": "dns://fw.example.org:9999/foo.bar.org?type=TXT", + "origin": "null", + "protocol": "dns:", + "username": "", + "password": "", + "host": "fw.example.org:9999", + "hostname": "fw.example.org", + "port": "9999", + "pathname": "/foo.bar.org", + "search": "?type=TXT", + "hash": "" + }, + { + "input": "ldap://localhost:389/ou=People,o=JNDITutorial", + "base": "about:blank", + "href": "ldap://localhost:389/ou=People,o=JNDITutorial", + "origin": "null", + "protocol": "ldap:", + "username": "", + "password": "", + "host": "localhost:389", + "hostname": "localhost", + "port": "389", + "pathname": "/ou=People,o=JNDITutorial", + "search": "", + "hash": "" + }, + { + "input": "git+https://github.com/foo/bar", + "base": "about:blank", + "href": "git+https://github.com/foo/bar", + "origin": "null", + "protocol": "git+https:", + "username": "", + "password": "", + "host": "github.com", + "hostname": "github.com", + "port": "", + "pathname": "/foo/bar", + "search": "", + "hash": "" + }, + { + "input": "urn:ietf:rfc:2648", + "base": "about:blank", + "href": "urn:ietf:rfc:2648", + "origin": "null", + "protocol": "urn:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "ietf:rfc:2648", + "search": "", + "hash": "" + }, + { + "input": "tag:joe@example.org,2001:foo/bar", + "base": "about:blank", + "href": "tag:joe@example.org,2001:foo/bar", + "origin": "null", + "protocol": "tag:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "joe@example.org,2001:foo/bar", + "search": "", + "hash": "" + }, + "# percent encoded hosts in non-special-URLs", + { + "input": "non-special://%E2%80%A0/", + "base": "about:blank", + "href": "non-special://%E2%80%A0/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "%E2%80%A0", + "hostname": "%E2%80%A0", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://H%4fSt/path", + "base": "about:blank", + "href": "non-special://H%4fSt/path", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "H%4fSt", + "hostname": "H%4fSt", + "port": "", + "pathname": "/path", + "search": "", + "hash": "" + }, + "# IPv6 in non-special-URLs", + { + "input": "non-special://[1:2:0:0:5:0:0:0]/", + "base": "about:blank", + "href": "non-special://[1:2:0:0:5::]/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2:0:0:5::]", + "hostname": "[1:2:0:0:5::]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[1:2:0:0:0:0:0:3]/", + "base": "about:blank", + "href": "non-special://[1:2::3]/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2::3]", + "hostname": "[1:2::3]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[1:2::3]:80/", + "base": "about:blank", + "href": "non-special://[1:2::3]:80/", + "protocol": "non-special:", + "username": "", + "password": "", + "host": "[1:2::3]:80", + "hostname": "[1:2::3]", + "port": "80", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "non-special://[:80/", + "base": "about:blank", + "failure": true + }, + { + "input": "blob:https://example.com:443/", + "base": "about:blank", + "href": "blob:https://example.com:443/", + "protocol": "blob:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "https://example.com:443/", + "search": "", + "hash": "" + }, + { + "input": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf", + "base": "about:blank", + "href": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf", + "protocol": "blob:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "d3958f5c-0777-0845-9dcf-2cb28783acaf", + "search": "", + "hash": "" + }, + "Invalid IPv4 radix digits", + { + "input": "http://0177.0.0.0189", + "base": "about:blank", + "href": "http://0177.0.0.0189/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0177.0.0.0189", + "hostname": "0177.0.0.0189", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0x7f.0.0.0x7g", + "base": "about:blank", + "href": "http://0x7f.0.0.0x7g/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0x7f.0.0.0x7g", + "hostname": "0x7f.0.0.0x7g", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://0X7F.0.0.0X7G", + "base": "about:blank", + "href": "http://0x7f.0.0.0x7g/", + "protocol": "http:", + "username": "", + "password": "", + "host": "0x7f.0.0.0x7g", + "hostname": "0x7f.0.0.0x7g", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Invalid IPv4 portion of IPv6 address", + { + "input": "http://[::127.0.0.0.1]", + "base": "about:blank", + "failure": true + }, + "Uncompressed IPv6 addresses with 0", + { + "input": "http://[0:1:0:1:0:1:0:1]", + "base": "about:blank", + "href": "http://[0:1:0:1:0:1:0:1]/", + "protocol": "http:", + "username": "", + "password": "", + "host": "[0:1:0:1:0:1:0:1]", + "hostname": "[0:1:0:1:0:1:0:1]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "http://[1:0:1:0:1:0:1:0]", + "base": "about:blank", + "href": "http://[1:0:1:0:1:0:1:0]/", + "protocol": "http:", + "username": "", + "password": "", + "host": "[1:0:1:0:1:0:1:0]", + "hostname": "[1:0:1:0:1:0:1:0]", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + "Percent-encoded query and fragment", + { + "input": "http://example.org/test?\u0022", + "base": "about:blank", + "href": "http://example.org/test?%22", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%22", + "hash": "" + }, + { + "input": "http://example.org/test?\u0023", + "base": "about:blank", + "href": "http://example.org/test?#", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "", + "hash": "" + }, + { + "input": "http://example.org/test?\u003C", + "base": "about:blank", + "href": "http://example.org/test?%3C", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%3C", + "hash": "" + }, + { + "input": "http://example.org/test?\u003E", + "base": "about:blank", + "href": "http://example.org/test?%3E", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%3E", + "hash": "" + }, + { + "input": "http://example.org/test?\u2323", + "base": "about:blank", + "href": "http://example.org/test?%E2%8C%A3", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%E2%8C%A3", + "hash": "" + }, + { + "input": "http://example.org/test?%23%23", + "base": "about:blank", + "href": "http://example.org/test?%23%23", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%23%23", + "hash": "" + }, + { + "input": "http://example.org/test?%GH", + "base": "about:blank", + "href": "http://example.org/test?%GH", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?%GH", + "hash": "" + }, + { + "input": "http://example.org/test?a#%EF", + "base": "about:blank", + "href": "http://example.org/test?a#%EF", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#%EF" + }, + { + "input": "http://example.org/test?a#%GH", + "base": "about:blank", + "href": "http://example.org/test?a#%GH", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#%GH" + }, + "Bad bases", + { + "input": "test-a.html", + "base": "a", + "failure": true + }, + { + "input": "test-a-slash.html", + "base": "a/", + "failure": true + }, + { + "input": "test-a-slash-slash.html", + "base": "a//", + "failure": true + }, + { + "input": "test-a-colon.html", + "base": "a:", + "failure": true + }, + { + "input": "test-a-colon-slash.html", + "base": "a:/", + "href": "a:/test-a-colon-slash.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test-a-colon-slash.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-slash-slash.html", + "base": "a://", + "href": "a:///test-a-colon-slash-slash.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test-a-colon-slash-slash.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-b.html", + "base": "a:b", + "failure": true + }, + { + "input": "test-a-colon-slash-b.html", + "base": "a:/b", + "href": "a:/test-a-colon-slash-b.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/test-a-colon-slash-b.html", + "search": "", + "hash": "" + }, + { + "input": "test-a-colon-slash-slash-b.html", + "base": "a://b", + "href": "a://b/test-a-colon-slash-slash-b.html", + "protocol": "a:", + "username": "", + "password": "", + "host": "b", + "hostname": "b", + "port": "", + "pathname": "/test-a-colon-slash-slash-b.html", + "search": "", + "hash": "" + }, + "Null code point in fragment", + { + "input": "http://example.org/test?a#b\u0000c", + "base": "about:blank", + "href": "http://example.org/test?a#bc", + "protocol": "http:", + "username": "", + "password": "", + "host": "example.org", + "hostname": "example.org", + "port": "", + "pathname": "/test", + "search": "?a", + "hash": "#bc" + }, + "New tests", + { + "input": "file:///foo/bar.html", + "base": "about:blank", + "href": "file:///foo/bar.html", + "origin": "file:///foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:///foo/bar.html?x=y", + "base": "about:blank", + "href": "file:///foo/bar.html?x=y", + "origin": "file:///foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "file:///foo/bar.html#bla", + "base": "about:blank", + "href": "file:///foo/bar.html#bla", + "origin": "file:///foo/bar.html", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/", + "search": "", + "hash": "" + }, + { + "input": "about:blank", + "base": "about:blank", + "href": "about:blank", + "origin": "null", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "about:home", + "base": "about:blank", + "href": "about:home", + "origin": "about:home", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "about:home#x", + "base": "about:blank", + "href": "about:home#x", + "origin": "about:home", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "about:home?x=y", + "base": "about:blank", + "href": "about:home?x=y", + "origin": "about:home", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "moz-safe-about:home", + "base": "about:blank", + "href": "moz-safe-about:home", + "origin": "moz-safe-about:home", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "moz-safe-about:home#x", + "base": "about:blank", + "href": "moz-safe-about:home#x", + "origin": "moz-safe-about:home", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "moz-safe-about:home?x=y", + "base": "about:blank", + "href": "moz-safe-about:home?x=y", + "origin": "moz-safe-about:home", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "indexeddb://fx-devtools", + "base": "about:blank", + "href": "indexeddb://fx-devtools/", + "origin": "indexeddb://fx-devtools", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "indexeddb://fx-devtools/", + "base": "about:blank", + "href": "indexeddb://fx-devtools/", + "origin": "indexeddb://fx-devtools", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "indexeddb://fx-devtools/foo", + "base": "about:blank", + "href": "indexeddb://fx-devtools/foo", + "origin": "indexeddb://fx-devtools", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "indexeddb://fx-devtools#x", + "base": "about:blank", + "href": "indexeddb://fx-devtools#x", + "origin": "indexeddb://fx-devtools", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "indexeddb://fx-devtools/#x", + "base": "about:blank", + "href": "indexeddb://fx-devtools/#x", + "origin": "indexeddb://fx-devtools", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc/foo/bar.html", + "base": "about:blank", + "href": "moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc/foo/bar.html", + "origin": "moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc:123/foo/bar.html", + "base": "about:blank", + "href": "moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc:123/foo/bar.html", + "origin": "moz-extension://53711a8f-65ed-e742-9671-1f02e267c0bc:123", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "resource://foo/bar.html", + "base": "about:blank", + "href": "resource://foo/bar.html", + "origin": "resource://foo", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + }, + { + "input": "resource://foo:123/bar.html", + "base": "about:blank", + "href": "resource://foo:123/bar.html", + "origin": "resource://foo:123", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "" + } +] diff --git a/netwerk/test/http3server/Cargo.toml b/netwerk/test/http3server/Cargo.toml new file mode 100644 index 0000000000..5094eaff14 --- /dev/null +++ b/netwerk/test/http3server/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "http3server" +version = "0.1.1" +authors = ["Dragana Damjanovic <dragana.damjano@gmail.com>"] +edition = "2018" +license = "MPL-2.0" + +[dependencies] +neqo-transport = { tag = "v0.6.4", git = "https://github.com/mozilla/neqo" } +neqo-common = { tag = "v0.6.4", git = "https://github.com/mozilla/neqo" } +neqo-http3 = { tag = "v0.6.4", git = "https://github.com/mozilla/neqo" } +neqo-qpack = { tag = "v0.6.4", git = "https://github.com/mozilla/neqo" } +mio = "0.6.17" +mio-extras = "2.0.5" +log = "0.4.0" +base64 = "0.21" +cfg-if = "1.0" +http = "0.2.8" +hyper = { version = "0.14", features = ["full"] } +tokio = { version = "1", features = ["full"] } + +[dependencies.neqo-crypto] +tag = "v0.6.4" +git = "https://github.com/mozilla/neqo" +default-features = false +features = ["gecko"] + +# Make sure to use bindgen's runtime-loading of libclang, as it allows for a wider range of clang versions to be used +[build-dependencies] +bindgen = {version = "0.64", default-features = false, features = ["runtime"] } + +[[bin]] +name = "http3server" +path = "src/main.rs" diff --git a/netwerk/test/http3server/moz.build b/netwerk/test/http3server/moz.build new file mode 100644 index 0000000000..9b96fae25e --- /dev/null +++ b/netwerk/test/http3server/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +if CONFIG["COMPILE_ENVIRONMENT"]: + RUST_PROGRAMS += [ + "http3server", + ] + +# Ideally, the build system would set @rpath to be @executable_path as +# a default for this executable so that this addition to LDFLAGS would not be +# needed here. Bug 1772575 is filed to implement that. +if CONFIG["OS_ARCH"] == "Darwin": + LDFLAGS += ["-Wl,-rpath,@executable_path"] diff --git a/netwerk/test/http3server/src/main.rs b/netwerk/test/http3server/src/main.rs new file mode 100644 index 0000000000..70cf8bb7ad --- /dev/null +++ b/netwerk/test/http3server/src/main.rs @@ -0,0 +1,1352 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![deny(warnings)] + +use base64::prelude::*; +use neqo_common::{event::Provider, qdebug, qinfo, qtrace, Datagram, Header}; +use neqo_crypto::{generate_ech_keys, init_db, AllowZeroRtt, AntiReplay}; +use neqo_http3::{ + Error, Http3OrWebTransportStream, Http3Parameters, Http3Server, Http3ServerEvent, + WebTransportRequest, WebTransportServerEvent, WebTransportSessionAcceptAction, +}; +use neqo_transport::server::{Server, ActiveConnectionRef}; +use neqo_transport::{ + ConnectionEvent, ConnectionParameters, Output, RandomConnectionIdGenerator, StreamId, + StreamType, +}; +use std::env; + +use std::cell::RefCell; +use std::io; +use std::path::PathBuf; +use std::process::exit; +use std::rc::Rc; +use std::thread; +use std::time::{Duration, Instant}; + +use cfg_if::cfg_if; +use core::fmt::Display; + +cfg_if! { + if #[cfg(not(target_os = "android"))] { + use std::sync::mpsc::{channel, Receiver, TryRecvError}; + use hyper::body::HttpBody; + use hyper::header::{HeaderName, HeaderValue}; + use hyper::{Body, Client, Method, Request}; + } +} + +use mio::net::UdpSocket; +use mio::{Events, Poll, PollOpt, Ready, Token}; +use mio_extras::timer::{Builder, Timeout, Timer}; +use std::cmp::{max, min}; +use std::collections::hash_map::DefaultHasher; +use std::collections::HashMap; +use std::collections::HashSet; +use std::hash::{Hash, Hasher}; +use std::mem; +use std::net::SocketAddr; + +const MAX_TABLE_SIZE: u64 = 65536; +const MAX_BLOCKED_STREAMS: u16 = 10; +const PROTOCOLS: &[&str] = &["h3-29", "h3"]; +const TIMER_TOKEN: Token = Token(0xffff); +const ECH_CONFIG_ID: u8 = 7; +const ECH_PUBLIC_NAME: &str = "public.example"; + +const HTTP_RESPONSE_WITH_WRONG_FRAME: &[u8] = &[ + 0x01, 0x06, 0x00, 0x00, 0xd9, 0x54, 0x01, 0x37, // headers + 0x0, 0x3, 0x61, 0x62, 0x63, // the first data frame + 0x3, 0x1, 0x5, // a cancel push frame that is not allowed +]; + +trait HttpServer: Display { + fn process(&mut self, dgram: Option<Datagram>) -> Output; + fn process_events(&mut self); + fn get_timeout(&self) -> Option<Duration> { + None + } +} + +struct Http3TestServer { + server: Http3Server, + // This a map from a post request to amount of data ithas been received on the request. + // The respons will carry the amount of data received. + posts: HashMap<Http3OrWebTransportStream, usize>, + responses: HashMap<Http3OrWebTransportStream, Vec<u8>>, + current_connection_hash: u64, + sessions_to_close: HashMap<Instant, Vec<WebTransportRequest>>, + sessions_to_create_stream: Vec<(WebTransportRequest, StreamType, bool)>, + webtransport_bidi_stream: HashSet<Http3OrWebTransportStream>, + wt_unidi_conn_to_stream: HashMap<ActiveConnectionRef, Http3OrWebTransportStream>, + wt_unidi_echo_back: HashMap<Http3OrWebTransportStream, Http3OrWebTransportStream>, +} + +impl ::std::fmt::Display for Http3TestServer { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "{}", self.server) + } +} + +impl Http3TestServer { + pub fn new(server: Http3Server) -> Self { + Self { + server, + posts: HashMap::new(), + responses: HashMap::new(), + current_connection_hash: 0, + sessions_to_close: HashMap::new(), + sessions_to_create_stream: Vec::new(), + webtransport_bidi_stream: HashSet::new(), + wt_unidi_conn_to_stream: HashMap::new(), + wt_unidi_echo_back: HashMap::new(), + } + } + + fn new_response(&mut self, mut stream: Http3OrWebTransportStream, mut data: Vec<u8>) { + if data.len() == 0 { + let _ = stream.stream_close_send(); + return; + } + match stream.send_data(&data) { + Ok(sent) => { + if sent < data.len() { + self.responses.insert(stream, data.split_off(sent)); + } else { + stream.stream_close_send().unwrap(); + } + } + Err(e) => { + eprintln!("error is {:?}", e); + } + } + } + + fn handle_stream_writable(&mut self, mut stream: Http3OrWebTransportStream) { + if let Some(data) = self.responses.get_mut(&stream) { + match stream.send_data(&data) { + Ok(sent) => { + if sent < data.len() { + let new_d = (*data).split_off(sent); + *data = new_d; + } else { + stream.stream_close_send().unwrap(); + self.responses.remove(&stream); + } + } + Err(_) => { + eprintln!("Unexpected error"); + } + } + } + } + + fn maybe_close_session(&mut self) { + let now = Instant::now(); + for (expires, sessions) in self.sessions_to_close.iter_mut() { + if *expires <= now { + for s in sessions.iter_mut() { + mem::drop(s.close_session(0, "")); + } + } + } + self.sessions_to_close.retain(|expires, _| *expires >= now); + } + + fn maybe_create_wt_stream(&mut self) { + if self.sessions_to_create_stream.is_empty() { + return; + } + let tuple = self.sessions_to_create_stream.pop().unwrap(); + let mut session = tuple.0; + let mut wt_server_stream = session.create_stream(tuple.1).unwrap(); + if tuple.1 == StreamType::UniDi { + if tuple.2 { + wt_server_stream.send_data(b"qwerty").unwrap(); + wt_server_stream.stream_close_send().unwrap(); + } else { + // relaying Http3ServerEvent::Data to uni streams + // slows down netwerk/test/unit/test_webtransport_simple.js + // to the point of failure. Only do so when necessary. + self.wt_unidi_conn_to_stream.insert(wt_server_stream.conn.clone(), wt_server_stream); + } + } else { + if tuple.2 { + wt_server_stream.send_data(b"asdfg").unwrap(); + wt_server_stream.stream_close_send().unwrap(); + wt_server_stream + .stream_stop_sending(Error::HttpNoError.code()) + .unwrap(); + } else { + self.webtransport_bidi_stream.insert(wt_server_stream); + } + } + } +} + +impl HttpServer for Http3TestServer { + fn process(&mut self, dgram: Option<Datagram>) -> Output { + self.server.process(dgram, Instant::now()) + } + + fn process_events(&mut self) { + self.maybe_close_session(); + self.maybe_create_wt_stream(); + + while let Some(event) = self.server.next_event() { + qtrace!("Event: {:?}", event); + match event { + Http3ServerEvent::Headers { + mut stream, + headers, + fin, + } => { + qtrace!("Headers (request={} fin={}): {:?}", stream, fin, headers); + + // Some responses do not have content-type. This is on purpose to exercise + // UnknownDecoder code. + let default_ret = b"Hello World".to_vec(); + let default_headers = vec![ + Header::new(":status", "200"), + Header::new("cache-control", "no-cache"), + Header::new("content-length", default_ret.len().to_string()), + Header::new( + "x-http3-conn-hash", + self.current_connection_hash.to_string(), + ), + ]; + + let path_hdr = headers.iter().find(|&h| h.name() == ":path"); + match path_hdr { + Some(ph) if !ph.value().is_empty() => { + let path = ph.value(); + qtrace!("Serve request {}", path); + if path == "/Response421" { + let response_body = b"0123456789".to_vec(); + stream + .send_headers(&[ + Header::new(":status", "421"), + Header::new("cache-control", "no-cache"), + Header::new("content-type", "text/plain"), + Header::new( + "content-length", + response_body.len().to_string(), + ), + ]) + .unwrap(); + self.new_response(stream, response_body); + } else if path == "/RequestCancelled" { + stream + .stream_stop_sending(Error::HttpRequestCancelled.code()) + .unwrap(); + stream + .stream_reset_send(Error::HttpRequestCancelled.code()) + .unwrap(); + } else if path == "/VersionFallback" { + stream + .stream_stop_sending(Error::HttpVersionFallback.code()) + .unwrap(); + stream + .stream_reset_send(Error::HttpVersionFallback.code()) + .unwrap(); + } else if path == "/EarlyResponse" { + stream + .stream_stop_sending(Error::HttpNoError.code()) + .unwrap(); + } else if path == "/RequestRejected" { + stream + .stream_stop_sending(Error::HttpRequestRejected.code()) + .unwrap(); + stream + .stream_reset_send(Error::HttpRequestRejected.code()) + .unwrap(); + } else if path == "/.well-known/http-opportunistic" { + let host_hdr = headers.iter().find(|&h| h.name() == ":authority"); + match host_hdr { + Some(host) if !host.value().is_empty() => { + let mut content = b"[\"http://".to_vec(); + content.extend(host.value().as_bytes()); + content.extend(b"\"]".to_vec()); + stream + .send_headers(&[ + Header::new(":status", "200"), + Header::new("cache-control", "no-cache"), + Header::new("content-type", "application/json"), + Header::new( + "content-length", + content.len().to_string(), + ), + ]) + .unwrap(); + self.new_response(stream, content); + } + _ => { + stream.send_headers(&default_headers).unwrap(); + self.new_response(stream, default_ret); + } + } + } else if path == "/no_body" { + stream + .send_headers(&[ + Header::new(":status", "200"), + Header::new("cache-control", "no-cache"), + ]) + .unwrap(); + stream.stream_close_send().unwrap(); + } else if path == "/no_content_length" { + stream + .send_headers(&[ + Header::new(":status", "200"), + Header::new("cache-control", "no-cache"), + ]) + .unwrap(); + self.new_response(stream, vec![b'a'; 4000]); + } else if path == "/content_length_smaller" { + stream + .send_headers(&[ + Header::new(":status", "200"), + Header::new("cache-control", "no-cache"), + Header::new("content-type", "text/plain"), + Header::new("content-length", 4000.to_string()), + ]) + .unwrap(); + self.new_response(stream, vec![b'a'; 8000]); + } else if path == "/post" { + // Read all data before responding. + self.posts.insert(stream, 0); + } else if path == "/priority_mirror" { + if let Some(priority) = + headers.iter().find(|h| h.name() == "priority") + { + stream + .send_headers(&[ + Header::new(":status", "200"), + Header::new("cache-control", "no-cache"), + Header::new("content-type", "text/plain"), + Header::new("priority-mirror", priority.value()), + Header::new( + "content-length", + priority.value().len().to_string(), + ), + ]) + .unwrap(); + self.new_response(stream, priority.value().as_bytes().to_vec()); + } else { + stream + .send_headers(&[ + Header::new(":status", "200"), + Header::new("cache-control", "no-cache"), + ]) + .unwrap(); + stream.stream_close_send().unwrap(); + } + } else if path == "/103_response" { + if let Some(early_hint) = + headers.iter().find(|h| h.name() == "link-to-set") + { + for l in early_hint.value().split(',') { + stream + .send_headers(&[ + Header::new(":status", "103"), + Header::new("link", l), + ]) + .unwrap(); + } + } + stream + .send_headers(&[ + Header::new(":status", "200"), + Header::new("cache-control", "no-cache"), + Header::new("content-length", "0"), + ]) + .unwrap(); + stream.stream_close_send().unwrap(); + } else { + match path.trim_matches(|p| p == '/').parse::<usize>() { + Ok(v) => { + stream + .send_headers(&[ + Header::new(":status", "200"), + Header::new("cache-control", "no-cache"), + Header::new("content-type", "text/plain"), + Header::new("content-length", v.to_string()), + ]) + .unwrap(); + self.new_response(stream, vec![b'a'; v]); + } + Err(_) => { + stream.send_headers(&default_headers).unwrap(); + self.new_response(stream, default_ret); + } + } + } + } + _ => { + stream.send_headers(&default_headers).unwrap(); + self.new_response(stream, default_ret); + } + } + } + Http3ServerEvent::Data { + mut stream, + data, + fin, + } => { + // echo bidirectional input back to client + if self.webtransport_bidi_stream.contains(&stream) { + self.new_response(stream, data); + break; + } + + // echo unidirectional input to back to client + // need to close or we hang + if self.wt_unidi_echo_back.contains_key(&stream) { + let mut echo_back = self.wt_unidi_echo_back.remove(&stream).unwrap(); + echo_back.send_data(&data).unwrap(); + echo_back.stream_close_send().unwrap(); + break; + } + + if let Some(r) = self.posts.get_mut(&stream) { + *r += data.len(); + } + if fin { + if let Some(r) = self.posts.remove(&stream) { + let default_ret = b"Hello World".to_vec(); + stream + .send_headers(&[ + Header::new(":status", "200"), + Header::new("cache-control", "no-cache"), + Header::new("x-data-received-length", r.to_string()), + Header::new("content-length", default_ret.len().to_string()), + ]) + .unwrap(); + self.new_response(stream, default_ret); + } + } + } + Http3ServerEvent::DataWritable { stream } => self.handle_stream_writable(stream), + Http3ServerEvent::StateChange { conn, state } => { + if matches!(state, neqo_http3::Http3State::Connected) { + let mut h = DefaultHasher::new(); + conn.hash(&mut h); + self.current_connection_hash = h.finish(); + } + } + Http3ServerEvent::PriorityUpdate { .. } => {} + Http3ServerEvent::StreamReset { stream, error } => { + qtrace!("Http3ServerEvent::StreamReset {:?} {:?}", stream, error); + } + Http3ServerEvent::StreamStopSending { stream, error } => { + qtrace!( + "Http3ServerEvent::StreamStopSending {:?} {:?}", + stream, + error + ); + } + Http3ServerEvent::WebTransport(WebTransportServerEvent::NewSession { + mut session, + headers, + }) => { + qdebug!( + "WebTransportServerEvent::NewSession {:?} {:?}", + session, + headers + ); + let path_hdr = headers.iter().find(|&h| h.name() == ":path"); + match path_hdr { + Some(ph) if !ph.value().is_empty() => { + let path = ph.value(); + qtrace!("Serve request {}", path); + if path == "/success" { + session + .response(&WebTransportSessionAcceptAction::Accept) + .unwrap(); + } else if path == "/redirect" { + session + .response(&WebTransportSessionAcceptAction::Reject( + [ + Header::new(":status", "302"), + Header::new("location", "/"), + ] + .to_vec(), + )) + .unwrap(); + } else if path == "/reject" { + session + .response(&WebTransportSessionAcceptAction::Reject( + [Header::new(":status", "404")].to_vec(), + )) + .unwrap(); + } else if path == "/closeafter0ms" { + session + .response(&WebTransportSessionAcceptAction::Accept) + .unwrap(); + let now = Instant::now(); + if !self.sessions_to_close.contains_key(&now) { + self.sessions_to_close.insert(now, Vec::new()); + } + self.sessions_to_close.get_mut(&now).unwrap().push(session); + } else if path == "/closeafter100ms" { + session + .response(&WebTransportSessionAcceptAction::Accept) + .unwrap(); + let expires = Instant::now() + Duration::from_millis(100); + if !self.sessions_to_close.contains_key(&expires) { + self.sessions_to_close.insert(expires, Vec::new()); + } + self.sessions_to_close + .get_mut(&expires) + .unwrap() + .push(session); + } else if path == "/create_unidi_stream" { + session + .response(&WebTransportSessionAcceptAction::Accept) + .unwrap(); + self.sessions_to_create_stream.push(( + session, + StreamType::UniDi, + false, + )); + } else if path == "/create_unidi_stream_and_hello" { + session + .response(&WebTransportSessionAcceptAction::Accept) + .unwrap(); + self.sessions_to_create_stream.push(( + session, + StreamType::UniDi, + true, + )); + } else if path == "/create_bidi_stream" { + session + .response(&WebTransportSessionAcceptAction::Accept) + .unwrap(); + self.sessions_to_create_stream.push(( + session, + StreamType::BiDi, + false, + )); + } else if path == "/create_bidi_stream_and_hello" { + self.webtransport_bidi_stream.clear(); + session + .response(&WebTransportSessionAcceptAction::Accept) + .unwrap(); + self.sessions_to_create_stream.push(( + session, + StreamType::BiDi, + true, + )); + } else { + session + .response(&WebTransportSessionAcceptAction::Accept) + .unwrap(); + } + } + _ => { + session + .response(&WebTransportSessionAcceptAction::Reject( + [Header::new(":status", "404")].to_vec(), + )) + .unwrap(); + } + } + } + Http3ServerEvent::WebTransport(WebTransportServerEvent::SessionClosed { + session, + reason, + headers: _, + }) => { + qdebug!( + "WebTransportServerEvent::SessionClosed {:?} {:?}", + session, + reason + ); + } + Http3ServerEvent::WebTransport(WebTransportServerEvent::NewStream(stream)) => { + // new stream could be from client-outgoing unidirectional + // or bidirectional + if !stream.stream_info.is_http() { + if stream.stream_id().is_bidi() { + self.webtransport_bidi_stream.insert(stream); + } else { + // Newly created stream happens on same connection + // as the stream creation for client's incoming stream. + // Link the streams with map for echo back + if self.wt_unidi_conn_to_stream.contains_key(&stream.conn) { + let s = self.wt_unidi_conn_to_stream.remove(&stream.conn).unwrap(); + self.wt_unidi_echo_back.insert(stream, s); + } + } + } + } + Http3ServerEvent::WebTransport(WebTransportServerEvent::Datagram { + mut session, + datagram, + }) => { + qdebug!( + "WebTransportServerEvent::Datagram {:?} {:?}", + session, + datagram + ); + session.send_datagram(datagram.as_ref(), None).unwrap(); + } + } + } + } + + fn get_timeout(&self) -> Option<Duration> { + if let Some(next) = self.sessions_to_close.keys().min() { + return Some(max(*next - Instant::now(), Duration::from_millis(0))); + } + None + } +} + +impl HttpServer for Server { + fn process(&mut self, dgram: Option<Datagram>) -> Output { + self.process(dgram, Instant::now()) + } + + fn process_events(&mut self) { + let active_conns = self.active_connections(); + for mut acr in active_conns { + loop { + let event = match acr.borrow_mut().next_event() { + None => break, + Some(e) => e, + }; + match event { + ConnectionEvent::RecvStreamReadable { stream_id } => { + if stream_id.is_bidi() && stream_id.is_client_initiated() { + // We are only interesting in request streams + acr.borrow_mut() + .stream_send(stream_id, HTTP_RESPONSE_WITH_WRONG_FRAME) + .expect("Read should succeed"); + } + } + _ => {} + } + } + } + } +} + +struct Http3ProxyServer { + server: Http3Server, + responses: HashMap<Http3OrWebTransportStream, Vec<u8>>, + server_port: i32, + request_header: HashMap<StreamId, Vec<Header>>, + request_body: HashMap<StreamId, Vec<u8>>, + #[cfg(not(target_os = "android"))] + stream_map: HashMap<StreamId, Http3OrWebTransportStream>, + #[cfg(not(target_os = "android"))] + response_to_send: HashMap<StreamId, Receiver<(Vec<Header>, Vec<u8>)>>, +} + +impl ::std::fmt::Display for Http3ProxyServer { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "{}", self.server) + } +} + +impl Http3ProxyServer { + pub fn new(server: Http3Server, server_port: i32) -> Self { + Self { + server, + responses: HashMap::new(), + server_port, + request_header: HashMap::new(), + request_body: HashMap::new(), + #[cfg(not(target_os = "android"))] + stream_map: HashMap::new(), + #[cfg(not(target_os = "android"))] + response_to_send: HashMap::new(), + } + } + + #[cfg(not(target_os = "android"))] + fn new_response(&mut self, mut stream: Http3OrWebTransportStream, mut data: Vec<u8>) { + if data.len() == 0 { + let _ = stream.stream_close_send(); + return; + } + match stream.send_data(&data) { + Ok(sent) => { + if sent < data.len() { + self.responses.insert(stream, data.split_off(sent)); + } else { + stream.stream_close_send().unwrap(); + } + } + Err(e) => { + eprintln!("error is {:?}", e); + } + } + } + + fn handle_stream_writable(&mut self, mut stream: Http3OrWebTransportStream) { + if let Some(data) = self.responses.get_mut(&stream) { + match stream.send_data(&data) { + Ok(sent) => { + if sent < data.len() { + let new_d = (*data).split_off(sent); + *data = new_d; + } else { + stream.stream_close_send().unwrap(); + self.responses.remove(&stream); + } + } + Err(_) => { + eprintln!("Unexpected error"); + } + } + } + } + + #[cfg(not(target_os = "android"))] + async fn fetch_url( + request: hyper::Request<Body>, + out_header: &mut Vec<Header>, + out_body: &mut Vec<u8>, + ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + let client = Client::new(); + let mut resp = client.request(request).await?; + out_header.push(Header::new(":status", resp.status().as_str())); + for (key, value) in resp.headers() { + out_header.push(Header::new( + key.as_str().to_ascii_lowercase(), + match value.to_str() { + Ok(str) => str, + _ => "", + }, + )); + } + + while let Some(chunk) = resp.body_mut().data().await { + match chunk { + Ok(data) => { + out_body.append(&mut data.to_vec()); + } + _ => {} + } + } + + Ok(()) + } + + #[cfg(not(target_os = "android"))] + fn fetch( + &mut self, + mut stream: Http3OrWebTransportStream, + request_headers: &Vec<Header>, + request_body: Vec<u8>, + ) { + let mut request: hyper::Request<Body> = Request::default(); + let mut path = String::new(); + for hdr in request_headers.iter() { + match hdr.name() { + ":method" => { + *request.method_mut() = Method::from_bytes(hdr.value().as_bytes()).unwrap(); + } + ":scheme" => {} + ":authority" => { + request.headers_mut().insert( + hyper::header::HOST, + HeaderValue::from_str(hdr.value()).unwrap(), + ); + } + ":path" => { + path = String::from(hdr.value()); + } + _ => { + if let Ok(hdr_name) = HeaderName::from_lowercase(hdr.name().as_bytes()) { + request + .headers_mut() + .insert(hdr_name, HeaderValue::from_str(hdr.value()).unwrap()); + } + } + } + } + *request.body_mut() = Body::from(request_body); + *request.uri_mut() = + match format!("http://127.0.0.1:{}{}", self.server_port.to_string(), path).parse() { + Ok(uri) => uri, + _ => { + eprintln!("invalid uri: {}", path); + stream + .send_headers(&[ + Header::new(":status", "400"), + Header::new("cache-control", "no-cache"), + Header::new("content-length", "0"), + ]) + .unwrap(); + return; + } + }; + qtrace!("request header: {:?}", request); + + let (sender, receiver) = channel(); + thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let mut h: Vec<Header> = Vec::new(); + let mut data: Vec<u8> = Vec::new(); + let _ = rt.block_on(Self::fetch_url(request, &mut h, &mut data)); + qtrace!("response headers: {:?}", h); + qtrace!("res data: {:02X?}", data); + + match sender.send((h, data)) { + Ok(()) => {} + _ => { + eprintln!("sender.send failed"); + } + } + }); + + self.response_to_send.insert(stream.stream_id(), receiver); + self.stream_map.insert(stream.stream_id(), stream); + } + + #[cfg(target_os = "android")] + fn fetch( + &mut self, + mut _stream: Http3OrWebTransportStream, + _request_headers: &Vec<Header>, + _request_body: Vec<u8>, + ) { + // do nothing + } + + #[cfg(not(target_os = "android"))] + fn maybe_process_response(&mut self) { + let mut data_to_send = HashMap::new(); + self.response_to_send + .retain(|id, receiver| match receiver.try_recv() { + Ok((headers, body)) => { + data_to_send.insert(*id, (headers.clone(), body.clone())); + false + } + Err(TryRecvError::Empty) => true, + Err(TryRecvError::Disconnected) => false, + }); + while let Some(id) = data_to_send.keys().next().cloned() { + let mut stream = self.stream_map.remove(&id).unwrap(); + let (header, data) = data_to_send.remove(&id).unwrap(); + qtrace!("response headers: {:?}", header); + match stream.send_headers(&header) { + Ok(()) => { + self.new_response(stream, data); + } + _ => {} + } + } + } +} + +impl HttpServer for Http3ProxyServer { + fn process(&mut self, dgram: Option<Datagram>) -> Output { + self.server.process(dgram, Instant::now()) + } + + fn process_events(&mut self) { + #[cfg(not(target_os = "android"))] + self.maybe_process_response(); + while let Some(event) = self.server.next_event() { + qtrace!("Event: {:?}", event); + match event { + Http3ServerEvent::Headers { + mut stream, + headers, + fin: _, + } => { + qtrace!("Headers {:?}", headers); + if self.server_port != -1 { + let method_hdr = headers.iter().find(|&h| h.name() == ":method"); + match method_hdr { + Some(method) => match method.value() { + "POST" => { + let content_length = + headers.iter().find(|&h| h.name() == "content-length"); + if let Some(length_str) = content_length { + if let Ok(len) = length_str.value().parse::<u32>() { + if len > 0 { + self.request_header + .insert(stream.stream_id(), headers); + self.request_body + .insert(stream.stream_id(), Vec::new()); + } else { + self.fetch(stream, &headers, b"".to_vec()); + } + } + } + } + _ => { + self.fetch(stream, &headers, b"".to_vec()); + } + }, + _ => {} + } + } else { + let path_hdr = headers.iter().find(|&h| h.name() == ":path"); + match path_hdr { + Some(ph) if !ph.value().is_empty() => { + let path = ph.value(); + match &path[..6] { + "/port?" => { + let port = path[6..].parse::<i32>(); + if let Ok(port) = port { + qtrace!("got port {}", port); + self.server_port = port; + } + } + _ => {} + } + } + _ => {} + } + stream + .send_headers(&[ + Header::new(":status", "200"), + Header::new("cache-control", "no-cache"), + Header::new("content-length", "0"), + ]) + .unwrap(); + } + } + Http3ServerEvent::Data { + stream, + mut data, + fin, + } => { + if let Some(d) = self.request_body.get_mut(&stream.stream_id()) { + d.append(&mut data); + } + if fin { + if let Some(d) = self.request_body.remove(&stream.stream_id()) { + let headers = self.request_header.remove(&stream.stream_id()).unwrap(); + self.fetch(stream, &headers, d); + } + } + } + Http3ServerEvent::DataWritable { stream } => self.handle_stream_writable(stream), + Http3ServerEvent::StateChange { .. } | Http3ServerEvent::PriorityUpdate { .. } => {} + Http3ServerEvent::StreamReset { stream, error } => { + qtrace!("Http3ServerEvent::StreamReset {:?} {:?}", stream, error); + } + Http3ServerEvent::StreamStopSending { stream, error } => { + qtrace!( + "Http3ServerEvent::StreamStopSending {:?} {:?}", + stream, + error + ); + } + Http3ServerEvent::WebTransport(_) => {} + } + } + } +} + +#[derive(Default)] +struct NonRespondingServer {} + +impl ::std::fmt::Display for NonRespondingServer { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "NonRespondingServer") + } +} + +impl HttpServer for NonRespondingServer { + fn process(&mut self, _dgram: Option<Datagram>) -> Output { + Output::None + } + + fn process_events(&mut self) {} +} + +fn emit_packet(socket: &UdpSocket, out_dgram: Datagram) { + let res = match socket.send_to(&out_dgram, &out_dgram.destination()) { + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => 0, + Err(err) => { + eprintln!("UDP send error: {:?}", err); + exit(1); + } + Ok(res) => res, + }; + if res != out_dgram.len() { + qinfo!("Unable to send all {} bytes of datagram", out_dgram.len()); + } +} + +fn process( + server: &mut dyn HttpServer, + svr_timeout: &mut Option<Timeout>, + inx: usize, + dgram: Option<Datagram>, + timer: &mut Timer<usize>, + socket: &mut UdpSocket, +) -> bool { + match server.process(dgram) { + Output::Datagram(dgram) => { + emit_packet(socket, dgram); + true + } + Output::Callback(mut new_timeout) => { + if let Some(t) = server.get_timeout() { + new_timeout = min(new_timeout, t); + } + if let Some(svr_timeout) = svr_timeout { + timer.cancel_timeout(svr_timeout); + } + + qinfo!("Setting timeout of {:?} for {}", new_timeout, server); + if new_timeout > Duration::from_secs(1) { + new_timeout = Duration::from_secs(1); + } + *svr_timeout = Some(timer.set_timeout(new_timeout, inx)); + false + } + Output::None => { + qdebug!("Output::None"); + false + } + } +} + +fn read_dgram( + socket: &mut UdpSocket, + local_address: &SocketAddr, +) -> Result<Option<Datagram>, io::Error> { + let buf = &mut [0u8; 2048]; + let res = socket.recv_from(&mut buf[..]); + if let Some(err) = res.as_ref().err() { + if err.kind() != io::ErrorKind::WouldBlock { + eprintln!("UDP recv error: {:?}", err); + } + return Ok(None); + }; + + let (sz, remote_addr) = res.unwrap(); + if sz == buf.len() { + eprintln!("Might have received more than {} bytes", buf.len()); + } + + if sz == 0 { + eprintln!("zero length datagram received?"); + Ok(None) + } else { + Ok(Some(Datagram::new(remote_addr, *local_address, &buf[..sz]))) + } +} + +enum ServerType { + Http3, + Http3Fail, + Http3NoResponse, + Http3Ech, + Http3Proxy, +} + +struct ServersRunner { + hosts: Vec<SocketAddr>, + poll: Poll, + sockets: Vec<UdpSocket>, + servers: HashMap<SocketAddr, (Box<dyn HttpServer>, Option<Timeout>)>, + timer: Timer<usize>, + active_servers: HashSet<usize>, + ech_config: Vec<u8>, +} + +impl ServersRunner { + pub fn new() -> Result<Self, io::Error> { + Ok(Self { + hosts: Vec::new(), + poll: Poll::new()?, + sockets: Vec::new(), + servers: HashMap::new(), + timer: Builder::default() + .tick_duration(Duration::from_millis(1)) + .build::<usize>(), + active_servers: HashSet::new(), + ech_config: Vec::new(), + }) + } + + pub fn init(&mut self) { + self.add_new_socket(0, ServerType::Http3, 0); + self.add_new_socket(1, ServerType::Http3Fail, 0); + self.add_new_socket(2, ServerType::Http3Ech, 0); + + let proxy_port = match env::var("MOZ_HTTP3_PROXY_PORT") { + Ok(val) => val.parse::<u16>().unwrap(), + _ => 0, + }; + self.add_new_socket(3, ServerType::Http3Proxy, proxy_port); + self.add_new_socket(5, ServerType::Http3NoResponse, 0); + + println!( + "HTTP3 server listening on ports {}, {}, {}, {} and {}. EchConfig is @{}@", + self.hosts[0].port(), + self.hosts[1].port(), + self.hosts[2].port(), + self.hosts[3].port(), + self.hosts[4].port(), + BASE64_STANDARD.encode(&self.ech_config) + ); + self.poll + .register(&self.timer, TIMER_TOKEN, Ready::readable(), PollOpt::edge()) + .unwrap(); + } + + fn add_new_socket(&mut self, count: usize, server_type: ServerType, port: u16) -> u16 { + let addr = format!("127.0.0.1:{}", port).parse().unwrap(); + + let socket = match UdpSocket::bind(&addr) { + Err(err) => { + eprintln!("Unable to bind UDP socket: {}", err); + exit(1) + } + Ok(s) => s, + }; + + let local_addr = match socket.local_addr() { + Err(err) => { + eprintln!("Socket local address not bound: {}", err); + exit(1) + } + Ok(s) => s, + }; + + self.hosts.push(local_addr); + + self.poll + .register( + &socket, + Token(count), + Ready::readable() | Ready::writable(), + PollOpt::edge(), + ) + .unwrap(); + + self.sockets.push(socket); + let server = self.create_server(server_type); + self.servers.insert(local_addr, (server, None)); + local_addr.port() + } + + fn create_server(&mut self, server_type: ServerType) -> Box<dyn HttpServer> { + let anti_replay = AntiReplay::new(Instant::now(), Duration::from_secs(10), 7, 14) + .expect("unable to setup anti-replay"); + let cid_mgr = Rc::new(RefCell::new(RandomConnectionIdGenerator::new(10))); + + match server_type { + ServerType::Http3 => Box::new(Http3TestServer::new( + Http3Server::new( + Instant::now(), + &[" HTTP2 Test Cert"], + PROTOCOLS, + anti_replay, + cid_mgr, + Http3Parameters::default() + .max_table_size_encoder(MAX_TABLE_SIZE) + .max_table_size_decoder(MAX_TABLE_SIZE) + .max_blocked_streams(MAX_BLOCKED_STREAMS) + .webtransport(true) + .connection_parameters(ConnectionParameters::default().datagram_size(1200)), + None, + ) + .expect("We cannot make a server!"), + )), + ServerType::Http3Fail => Box::new( + Server::new( + Instant::now(), + &[" HTTP2 Test Cert"], + PROTOCOLS, + anti_replay, + Box::new(AllowZeroRtt {}), + cid_mgr, + ConnectionParameters::default(), + ) + .expect("We cannot make a server!"), + ), + ServerType::Http3NoResponse => Box::new(NonRespondingServer::default()), + ServerType::Http3Ech => { + let mut server = Box::new(Http3TestServer::new( + Http3Server::new( + Instant::now(), + &[" HTTP2 Test Cert"], + PROTOCOLS, + anti_replay, + cid_mgr, + Http3Parameters::default() + .max_table_size_encoder(MAX_TABLE_SIZE) + .max_table_size_decoder(MAX_TABLE_SIZE) + .max_blocked_streams(MAX_BLOCKED_STREAMS), + None, + ) + .expect("We cannot make a server!"), + )); + let ref mut unboxed_server = (*server).server; + let (sk, pk) = generate_ech_keys().unwrap(); + unboxed_server + .enable_ech(ECH_CONFIG_ID, ECH_PUBLIC_NAME, &sk, &pk) + .expect("unable to enable ech"); + self.ech_config = Vec::from(unboxed_server.ech_config()); + server + } + ServerType::Http3Proxy => { + let server_config = if env::var("MOZ_HTTP3_MOCHITEST").is_ok() { + ("mochitest-cert", 8888) + } else { + (" HTTP2 Test Cert", -1) + }; + let server = Box::new(Http3ProxyServer::new( + Http3Server::new( + Instant::now(), + &[server_config.0], + PROTOCOLS, + anti_replay, + cid_mgr, + Http3Parameters::default() + .max_table_size_encoder(MAX_TABLE_SIZE) + .max_table_size_decoder(MAX_TABLE_SIZE) + .max_blocked_streams(MAX_BLOCKED_STREAMS) + .webtransport(true) + .connection_parameters( + ConnectionParameters::default().datagram_size(1200), + ), + None, + ) + .expect("We cannot make a server!"), + server_config.1, + )); + server + } + } + } + + fn process_datagrams_and_events( + &mut self, + inx: usize, + read_socket: bool, + ) -> Result<(), io::Error> { + if let Some(socket) = self.sockets.get_mut(inx) { + if let Some((ref mut server, svr_timeout)) = + self.servers.get_mut(&socket.local_addr().unwrap()) + { + if read_socket { + loop { + let dgram = read_dgram(socket, &self.hosts[inx])?; + if dgram.is_none() { + break; + } + let _ = process( + &mut **server, + svr_timeout, + inx, + dgram, + &mut self.timer, + socket, + ); + } + } else { + let _ = process( + &mut **server, + svr_timeout, + inx, + None, + &mut self.timer, + socket, + ); + } + server.process_events(); + if process( + &mut **server, + svr_timeout, + inx, + None, + &mut self.timer, + socket, + ) { + self.active_servers.insert(inx); + } + } + } + Ok(()) + } + + fn process_active_conns(&mut self) -> Result<(), io::Error> { + let curr_active = mem::take(&mut self.active_servers); + for inx in curr_active { + self.process_datagrams_and_events(inx, false)?; + } + Ok(()) + } + + fn process_timeout(&mut self) -> Result<(), io::Error> { + while let Some(inx) = self.timer.poll() { + qinfo!("Timer expired for {:?}", inx); + self.process_datagrams_and_events(inx, false)?; + } + Ok(()) + } + + pub fn run(&mut self) -> Result<(), io::Error> { + let mut events = Events::with_capacity(1024); + loop { + // If there are active servers do not block in poll. + self.poll.poll( + &mut events, + if self.active_servers.is_empty() { + None + } else { + Some(Duration::from_millis(0)) + }, + )?; + + for event in &events { + if event.token() == TIMER_TOKEN { + self.process_timeout()?; + } else { + self.process_datagrams_and_events( + event.token().0, + event.readiness().is_readable(), + )?; + } + } + self.process_active_conns()?; + } + } +} + +fn main() -> Result<(), io::Error> { + let args: Vec<String> = env::args().collect(); + if args.len() < 2 { + eprintln!("Wrong arguments."); + exit(1) + } + + // Read data from stdin and terminate the server if EOF is detected, which + // means that runxpcshelltests.py ended without shutting down the server. + thread::spawn(|| loop { + let mut buffer = String::new(); + match io::stdin().read_line(&mut buffer) { + Ok(n) => { + if n == 0 { + exit(0); + } + } + Err(_) => { + exit(0); + } + } + }); + + init_db(PathBuf::from(args[1].clone())); + + let mut servers_runner = ServersRunner::new()?; + servers_runner.init(); + servers_runner.run() +} diff --git a/netwerk/test/http3serverDB/cert9.db b/netwerk/test/http3serverDB/cert9.db Binary files differnew file mode 100644 index 0000000000..173c4fff61 --- /dev/null +++ b/netwerk/test/http3serverDB/cert9.db diff --git a/netwerk/test/http3serverDB/key4.db b/netwerk/test/http3serverDB/key4.db Binary files differnew file mode 100644 index 0000000000..a06bec3684 --- /dev/null +++ b/netwerk/test/http3serverDB/key4.db diff --git a/netwerk/test/http3serverDB/pkcs11.txt b/netwerk/test/http3serverDB/pkcs11.txt new file mode 100644 index 0000000000..2f1a4bfb5b --- /dev/null +++ b/netwerk/test/http3serverDB/pkcs11.txt @@ -0,0 +1,4 @@ +library= +name=NSS Internal PKCS #11 Module +parameters=configdir='.' certPrefix='' keyPrefix='' secmod='' flags= updatedir='' updateCertPrefix='' updateKeyPrefix='' updateid='' updateTokenDescription='' +NSS=Flags=internal,critical trustOrder=75 cipherOrder=100 slotParams=(1={slotFlags=[ECC,RSA,DSA,DH,RC2,RC4,DES,RANDOM,SHA1,MD5,MD2,SSL,TLS,AES,Camellia,SEED,SHA256,SHA512] askpw=any timeout=30}) diff --git a/netwerk/test/httpserver/README b/netwerk/test/httpserver/README new file mode 100644 index 0000000000..97624789ca --- /dev/null +++ b/netwerk/test/httpserver/README @@ -0,0 +1,101 @@ +httpd.js README +=============== + +httpd.js is a small cross-platform implementation of an HTTP/1.1 server in +JavaScript for the Mozilla platform. + +httpd.js may be used as an XPCOM component, as an inline script in a document +with XPCOM privileges, or from the XPCOM shell (xpcshell). Currently, its most- +supported method of use is from the XPCOM shell, where you can get all the +dynamicity of JS in adding request handlers and the like, but component-based +equivalent functionality is planned. + + +Using httpd.js as an XPCOM Component +------------------------------------ + +First, create an XPT file for nsIHttpServer.idl, using the xpidl tool included +in the Mozilla SDK for the environment in which you wish to run httpd.js. See +<http://developer.mozilla.org/en/docs/XPIDL:xpidl> for further details on how to +do this. + +Next, register httpd.js and nsIHttpServer.xpt in your Mozilla application. In +Firefox, these simply need to be added to the /components directory of your XPI. +Other applications may require use of regxpcom or other techniques; consult the +applicable documentation for further details. + +Finally, load httpd.js into the current file, and create an instance of the +server using the following command: + + var server = new nsHttpServer(); + +At this point you'll want to initialize the server, since by default it doesn't +serve many useful paths. For more information on this, see the IDL docs for the +nsIHttpServer interface in nsIHttpServer.idl, particularly for +registerDirectory (useful for mapping the contents of directories onto request +paths), registerPathHandler (for setting a custom handler for a specific path on +the server, such as CGI functionality), and registerFile (for mapping a file to +a specific path). + +Finally, you'll want to start (and later stop) the server. Here's some example +code which does this: + + server.start(8080); // port on which server will operate + + // ...server now runs and serves requests... + + server.stop(); + +This server will only respond to requests on 127.0.0.1:8080 or localhost:8080. +If you want it to respond to requests at different hosts (say via a proxy +mechanism), you must use server.identity.add() or server.identity.setPrimary() +to add it. + + +Using httpd.js as an Inline Script or from xpcshell +--------------------------------------------------- + +Using httpd.js as a script or from xpcshell isn't very different from using it +as a component; the only real difference lies in how you create an instance of +the server. To create an instance, do the following: + + var server = new nsHttpServer(); + +You now can use |server| exactly as you would when |server| was created as an +XPCOM component. Note, however, that doing so will trample over the global +namespace, and global values defined in httpd.js will leak into your script. +This may typically be benign, but since some of the global values defined are +constants (specifically, Cc/Ci/Cr as abbreviations for the classes, interfaces, +and results properties of Components), it's possible this trampling could +break your script. In general you should use httpd.js as an XPCOM component +whenever possible. + + +Known Issues +------------ + +httpd.js makes no effort to time out requests, beyond any the socket itself +might or might not provide. I don't believe it provides any by default, but +I haven't verified this. + +Every incoming request is processed by the corresponding request handler +synchronously. In other words, once the first CRLFCRLF of a request is +received, the entire response is created before any new incoming requests can be +served. I anticipate adding asynchronous handler functionality in bug 396226, +but it may be some time before that happens. + +There is no way to access the body of an incoming request. This problem is +merely a symptom of the previous one, and they will probably both be addressed +at the same time. + + +Other Goodies +------------- + +A special testing function, |server|, is provided for use in xpcshell for quick +testing of the server; see the source code for details on its use. You don't +want to use this in a script, however, because doing so will block until the +server is shut down. It's also a good example of how to use the basic +functionality of httpd.js, if you need one. + +Have fun! diff --git a/netwerk/test/httpserver/TODO b/netwerk/test/httpserver/TODO new file mode 100644 index 0000000000..3a95466117 --- /dev/null +++ b/netwerk/test/httpserver/TODO @@ -0,0 +1,17 @@ +Bugs to fix: +- make content-length generation not rely on .available() returning the entire + size of the body stream's contents -- some sort of wrapper (but how does that + work for the unscriptable method WriteSegments, which is good to support from + a performance standpoint?) + +Ideas for future improvements: +- add API to disable response buffering which, when called, causes errors when + you try to do anything other than write to the body stream (i.e., modify + headers or status line) once you've written anything to it -- useful when + storing the entire response in memory is unfeasible (e.g., you're testing + >4GB download characteristics) +- add an API which performs asynchronous response processing (instead of + nsIHttpRequestHandler.handle, which must construct the response before control + returns; |void asyncHandle(request, response)|) -- useful, and can it be done + in JS? +- other awesomeness? diff --git a/netwerk/test/httpserver/httpd.js b/netwerk/test/httpserver/httpd.js new file mode 100644 index 0000000000..deee9a3fd3 --- /dev/null +++ b/netwerk/test/httpserver/httpd.js @@ -0,0 +1,5655 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* + * An implementation of an HTTP server both as a loadable script and as an XPCOM + * component. See the accompanying README file for user documentation on + * httpd.js. + */ + +var EXPORTED_SYMBOLS = [ + "HTTP_400", + "HTTP_401", + "HTTP_402", + "HTTP_403", + "HTTP_404", + "HTTP_405", + "HTTP_406", + "HTTP_407", + "HTTP_408", + "HTTP_409", + "HTTP_410", + "HTTP_411", + "HTTP_412", + "HTTP_413", + "HTTP_414", + "HTTP_415", + "HTTP_417", + "HTTP_500", + "HTTP_501", + "HTTP_502", + "HTTP_503", + "HTTP_504", + "HTTP_505", + "HttpError", + "HttpServer", + "NodeServer", +]; + +const CC = Components.Constructor; + +const PR_UINT32_MAX = Math.pow(2, 32) - 1; + +/** True if debugging output is enabled, false otherwise. */ +var DEBUG = false; // non-const *only* so tweakable in server tests + +/** True if debugging output should be timestamped. */ +var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +/** + * Asserts that the given condition holds. If it doesn't, the given message is + * dumped, a stack trace is printed, and an exception is thrown to attempt to + * stop execution (which unfortunately must rely upon the exception not being + * accidentally swallowed by the code that uses it). + */ +function NS_ASSERT(cond, msg) { + if (DEBUG && !cond) { + dumpn("###!!!"); + dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!")); + dumpn("###!!! Stack follows:"); + + var stack = new Error().stack.split(/\n/); + dumpn( + stack + .map(function (val) { + return "###!!! " + val; + }) + .join("\n") + ); + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + } +} + +/** Constructs an HTTP error object. */ +function HttpError(code, description) { + this.code = code; + this.description = description; +} +HttpError.prototype = { + toString() { + return this.code + " " + this.description; + }, +}; + +/** + * Errors thrown to trigger specific HTTP server responses. + */ +var HTTP_400 = new HttpError(400, "Bad Request"); +var HTTP_401 = new HttpError(401, "Unauthorized"); +var HTTP_402 = new HttpError(402, "Payment Required"); +var HTTP_403 = new HttpError(403, "Forbidden"); +var HTTP_404 = new HttpError(404, "Not Found"); +var HTTP_405 = new HttpError(405, "Method Not Allowed"); +var HTTP_406 = new HttpError(406, "Not Acceptable"); +var HTTP_407 = new HttpError(407, "Proxy Authentication Required"); +var HTTP_408 = new HttpError(408, "Request Timeout"); +var HTTP_409 = new HttpError(409, "Conflict"); +var HTTP_410 = new HttpError(410, "Gone"); +var HTTP_411 = new HttpError(411, "Length Required"); +var HTTP_412 = new HttpError(412, "Precondition Failed"); +var HTTP_413 = new HttpError(413, "Request Entity Too Large"); +var HTTP_414 = new HttpError(414, "Request-URI Too Long"); +var HTTP_415 = new HttpError(415, "Unsupported Media Type"); +var HTTP_417 = new HttpError(417, "Expectation Failed"); + +var HTTP_500 = new HttpError(500, "Internal Server Error"); +var HTTP_501 = new HttpError(501, "Not Implemented"); +var HTTP_502 = new HttpError(502, "Bad Gateway"); +var HTTP_503 = new HttpError(503, "Service Unavailable"); +var HTTP_504 = new HttpError(504, "Gateway Timeout"); +var HTTP_505 = new HttpError(505, "HTTP Version Not Supported"); + +/** Creates a hash with fields corresponding to the values in arr. */ +function array2obj(arr) { + var obj = {}; + for (var i = 0; i < arr.length; i++) { + obj[arr[i]] = arr[i]; + } + return obj; +} + +/** Returns an array of the integers x through y, inclusive. */ +function range(x, y) { + var arr = []; + for (var i = x; i <= y; i++) { + arr.push(i); + } + return arr; +} + +/** An object (hash) whose fields are the numbers of all HTTP error codes. */ +const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505))); + +/** + * The character used to distinguish hidden files from non-hidden files, a la + * the leading dot in Apache. Since that mechanism also hides files from + * easy display in LXR, ls output, etc. however, we choose instead to use a + * suffix character. If a requested file ends with it, we append another + * when getting the file on the server. If it doesn't, we just look up that + * file. Therefore, any file whose name ends with exactly one of the character + * is "hidden" and available for use by the server. + */ +const HIDDEN_CHAR = "^"; + +/** + * The file name suffix indicating the file containing overridden headers for + * a requested file. + */ +const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR; +const INFORMATIONAL_RESPONSE_SUFFIX = + HIDDEN_CHAR + "informationalResponse" + HIDDEN_CHAR; + +/** Type used to denote SJS scripts for CGI-like functionality. */ +const SJS_TYPE = "sjs"; + +/** Base for relative timestamps produced by dumpn(). */ +var firstStamp = 0; + +/** dump(str) with a trailing "\n" -- only outputs if DEBUG. */ +function dumpn(str) { + if (DEBUG) { + var prefix = "HTTPD-INFO | "; + if (DEBUG_TIMESTAMP) { + if (firstStamp === 0) { + firstStamp = Date.now(); + } + + var elapsed = Date.now() - firstStamp; // milliseconds + var min = Math.floor(elapsed / 60000); + var sec = (elapsed % 60000) / 1000; + + if (sec < 10) { + prefix += min + ":0" + sec.toFixed(3) + " | "; + } else { + prefix += min + ":" + sec.toFixed(3) + " | "; + } + } + + dump(prefix + str + "\n"); + } +} + +/** Dumps the current JS stack if DEBUG. */ +function dumpStack() { + // peel off the frames for dumpStack() and Error() + var stack = new Error().stack.split(/\n/).slice(2); + stack.forEach(dumpn); +} + +/** The XPCOM thread manager. */ +var gThreadManager = null; + +/** + * JavaScript constructors for commonly-used classes; precreating these is a + * speedup over doing the same from base principles. See the docs at + * http://developer.mozilla.org/en/docs/Components.Constructor for details. + */ +const ServerSocket = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init" +); +const ServerSocketIPv6 = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "initIPv6" +); +const ServerSocketDualStack = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "initDualStack" +); +const ScriptableInputStream = CC( + "@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", + "init" +); +const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init"); +const FileInputStream = CC( + "@mozilla.org/network/file-input-stream;1", + "nsIFileInputStream", + "init" +); +const ConverterInputStream = CC( + "@mozilla.org/intl/converter-input-stream;1", + "nsIConverterInputStream", + "init" +); +const WritablePropertyBag = CC( + "@mozilla.org/hash-property-bag;1", + "nsIWritablePropertyBag2" +); +const SupportsString = CC( + "@mozilla.org/supports-string;1", + "nsISupportsString" +); + +/* These two are non-const only so a test can overwrite them. */ +var BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +var BinaryOutputStream = CC( + "@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream" +); + +/** + * Returns the RFC 822/1123 representation of a date. + * + * @param date : Number + * the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT + * @returns string + * the representation of the given date + */ +function toDateString(date) { + // + // rfc1123-date = wkday "," SP date1 SP time SP "GMT" + // date1 = 2DIGIT SP month SP 4DIGIT + // ; day month year (e.g., 02 Jun 1982) + // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT + // ; 00:00:00 - 23:59:59 + // wkday = "Mon" | "Tue" | "Wed" + // | "Thu" | "Fri" | "Sat" | "Sun" + // month = "Jan" | "Feb" | "Mar" | "Apr" + // | "May" | "Jun" | "Jul" | "Aug" + // | "Sep" | "Oct" | "Nov" | "Dec" + // + + const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const monthStrings = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + + /** + * Processes a date and returns the encoded UTC time as a string according to + * the format specified in RFC 2616. + * + * @param date : Date + * the date to process + * @returns string + * a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" + */ + function toTime(date) { + var hrs = date.getUTCHours(); + var rv = hrs < 10 ? "0" + hrs : hrs; + + var mins = date.getUTCMinutes(); + rv += ":"; + rv += mins < 10 ? "0" + mins : mins; + + var secs = date.getUTCSeconds(); + rv += ":"; + rv += secs < 10 ? "0" + secs : secs; + + return rv; + } + + /** + * Processes a date and returns the encoded UTC date as a string according to + * the date1 format specified in RFC 2616. + * + * @param date : Date + * the date to process + * @returns string + * a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" + */ + function toDate1(date) { + var day = date.getUTCDate(); + var month = date.getUTCMonth(); + var year = date.getUTCFullYear(); + + var rv = day < 10 ? "0" + day : day; + rv += " " + monthStrings[month]; + rv += " " + year; + + return rv; + } + + date = new Date(date); + + const fmtString = "%wkday%, %date1% %time% GMT"; + var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]); + rv = rv.replace("%time%", toTime(date)); + return rv.replace("%date1%", toDate1(date)); +} + +/** + * Prints out a human-readable representation of the object o and its fields, + * omitting those whose names begin with "_" if showMembers != true (to ignore + * "private" properties exposed via getters/setters). + */ +function printObj(o, showMembers) { + var s = "******************************\n"; + s += "o = {\n"; + for (var i in o) { + if (typeof i != "string" || showMembers || (!!i.length && i[0] != "_")) { + s += " " + i + ": " + o[i] + ",\n"; + } + } + s += " };\n"; + s += "******************************"; + dumpn(s); +} + +/** + * Instantiates a new HTTP server. + */ +function nsHttpServer() { + if (!gThreadManager) { + gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + } + + /** The port on which this server listens. */ + this._port = undefined; + + /** The socket associated with this. */ + this._socket = null; + + /** The handler used to process requests to this server. */ + this._handler = new ServerHandler(this); + + /** Naming information for this server. */ + this._identity = new ServerIdentity(); + + /** + * Indicates when the server is to be shut down at the end of the request. + */ + this._doQuit = false; + + /** + * True if the socket in this is closed (and closure notifications have been + * sent and processed if the socket was ever opened), false otherwise. + */ + this._socketClosed = true; + + /** + * Used for tracking existing connections and ensuring that all connections + * are properly cleaned up before server shutdown; increases by 1 for every + * new incoming connection. + */ + this._connectionGen = 0; + + /** + * Hash of all open connections, indexed by connection number at time of + * creation. + */ + this._connections = {}; +} +nsHttpServer.prototype = { + // NSISERVERSOCKETLISTENER + + /** + * Processes an incoming request coming in on the given socket and contained + * in the given transport. + * + * @param socket : nsIServerSocket + * the socket through which the request was served + * @param trans : nsISocketTransport + * the transport for the request/response + * @see nsIServerSocketListener.onSocketAccepted + */ + onSocketAccepted(socket, trans) { + dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")"); + + dumpn(">>> new connection on " + trans.host + ":" + trans.port); + + const SEGMENT_SIZE = 8192; + const SEGMENT_COUNT = 1024; + try { + var input = trans + .openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT) + .QueryInterface(Ci.nsIAsyncInputStream); + var output = trans.openOutputStream(0, 0, 0); + } catch (e) { + dumpn("*** error opening transport streams: " + e); + trans.close(Cr.NS_BINDING_ABORTED); + return; + } + + var connectionNumber = ++this._connectionGen; + + try { + var conn = new Connection( + input, + output, + this, + socket.port, + trans.port, + connectionNumber, + trans + ); + var reader = new RequestReader(conn); + + // XXX add request timeout functionality here! + + // Note: must use main thread here, or we might get a GC that will cause + // threadsafety assertions. We really need to fix XPConnect so that + // you can actually do things in multi-threaded JS. :-( + input.asyncWait(reader, 0, 0, gThreadManager.mainThread); + } catch (e) { + // Assume this connection can't be salvaged and bail on it completely; + // don't attempt to close it so that we can assert that any connection + // being closed is in this._connections. + dumpn("*** error in initial request-processing stages: " + e); + trans.close(Cr.NS_BINDING_ABORTED); + return; + } + + this._connections[connectionNumber] = conn; + dumpn("*** starting connection " + connectionNumber); + }, + + /** + * Called when the socket associated with this is closed. + * + * @param socket : nsIServerSocket + * the socket being closed + * @param status : nsresult + * the reason the socket stopped listening (NS_BINDING_ABORTED if the server + * was stopped using nsIHttpServer.stop) + * @see nsIServerSocketListener.onStopListening + */ + onStopListening(socket, status) { + dumpn(">>> shutting down server on port " + socket.port); + for (var n in this._connections) { + if (!this._connections[n]._requestStarted) { + this._connections[n].close(); + } + } + this._socketClosed = true; + if (this._hasOpenConnections()) { + dumpn("*** open connections!!!"); + } + if (!this._hasOpenConnections()) { + dumpn("*** no open connections, notifying async from onStopListening"); + + // Notify asynchronously so that any pending teardown in stop() has a + // chance to run first. + var self = this; + var stopEvent = { + run() { + dumpn("*** _notifyStopped async callback"); + self._notifyStopped(); + }, + }; + gThreadManager.currentThread.dispatch( + stopEvent, + Ci.nsIThread.DISPATCH_NORMAL + ); + } + }, + + // NSIHTTPSERVER + + // + // see nsIHttpServer.start + // + start(port) { + this._start(port, "localhost"); + }, + + // + // see nsIHttpServer.start_ipv6 + // + start_ipv6(port) { + this._start(port, "[::1]"); + }, + + start_dualStack(port) { + this._start(port, "[::1]", true); + }, + + _start(port, host, dualStack) { + if (this._socket) { + throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED); + } + + this._port = port; + this._doQuit = this._socketClosed = false; + + this._host = host; + + // The listen queue needs to be long enough to handle + // network.http.max-persistent-connections-per-server or + // network.http.max-persistent-connections-per-proxy concurrent + // connections, plus a safety margin in case some other process is + // talking to the server as well. + var maxConnections = + 5 + + Math.max( + Services.prefs.getIntPref( + "network.http.max-persistent-connections-per-server" + ), + Services.prefs.getIntPref( + "network.http.max-persistent-connections-per-proxy" + ) + ); + + try { + var loopback = true; + if ( + this._host != "127.0.0.1" && + this._host != "localhost" && + this._host != "[::1]" + ) { + loopback = false; + } + + // When automatically selecting a port, sometimes the chosen port is + // "blocked" from clients. We don't want to use these ports because + // tests will intermittently fail. So, we simply keep trying to to + // get a server socket until a valid port is obtained. We limit + // ourselves to finite attempts just so we don't loop forever. + var socket; + for (var i = 100; i; i--) { + var temp = null; + if (dualStack) { + temp = new ServerSocketDualStack(this._port, maxConnections); + } else if (this._host.includes(":")) { + temp = new ServerSocketIPv6( + this._port, + loopback, // true = localhost, false = everybody + maxConnections + ); + } else { + temp = new ServerSocket( + this._port, + loopback, // true = localhost, false = everybody + maxConnections + ); + } + + var allowed = Services.io.allowPort(temp.port, "http"); + if (!allowed) { + dumpn( + ">>>Warning: obtained ServerSocket listens on a blocked " + + "port: " + + temp.port + ); + } + + if (!allowed && this._port == -1) { + dumpn(">>>Throwing away ServerSocket with bad port."); + temp.close(); + continue; + } + + socket = temp; + break; + } + + if (!socket) { + throw new Error( + "No socket server available. Are there no available ports?" + ); + } + + socket.asyncListen(this); + this._port = socket.port; + this._identity._initialize(socket.port, host, true, dualStack); + this._socket = socket; + dumpn( + ">>> listening on port " + + socket.port + + ", " + + maxConnections + + " pending connections" + ); + } catch (e) { + dump("\n!!! could not start server on port " + port + ": " + e + "\n\n"); + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + }, + + // + // see nsIHttpServer.stop + // + stop(callback) { + if (!this._socket) { + throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); + } + + // If no argument was provided to stop, return a promise. + let returnValue = undefined; + if (!callback) { + returnValue = new Promise(resolve => { + callback = resolve; + }); + } + + this._stopCallback = + typeof callback === "function" + ? callback + : function () { + callback.onStopped(); + }; + + dumpn(">>> stopping listening on port " + this._socket.port); + this._socket.close(); + this._socket = null; + + // We can't have this identity any more, and the port on which we're running + // this server now could be meaningless the next time around. + this._identity._teardown(); + + this._doQuit = false; + + // socket-close notification and pending request completion happen async + + return returnValue; + }, + + // + // see nsIHttpServer.registerFile + // + registerFile(path, file, handler) { + if (file && (!file.exists() || file.isDirectory())) { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + this._handler.registerFile(path, file, handler); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory(path, directory) { + // XXX true path validation! + if ( + path.charAt(0) != "/" || + path.charAt(path.length - 1) != "/" || + (directory && (!directory.exists() || !directory.isDirectory())) + ) { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping + // exists! + + this._handler.registerDirectory(path, directory); + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler(path, handler) { + this._handler.registerPathHandler(path, handler); + }, + + // + // see nsIHttpServer.registerPrefixHandler + // + registerPrefixHandler(prefix, handler) { + this._handler.registerPrefixHandler(prefix, handler); + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler(code, handler) { + this._handler.registerErrorHandler(code, handler); + }, + + // + // see nsIHttpServer.setIndexHandler + // + setIndexHandler(handler) { + this._handler.setIndexHandler(handler); + }, + + // + // see nsIHttpServer.registerContentType + // + registerContentType(ext, type) { + this._handler.registerContentType(ext, type); + }, + + get connectionNumber() { + return this._connectionGen; + }, + + // + // see nsIHttpServer.serverIdentity + // + get identity() { + return this._identity; + }, + + // + // see nsIHttpServer.getState + // + getState(path, k) { + return this._handler._getState(path, k); + }, + + // + // see nsIHttpServer.setState + // + setState(path, k, v) { + return this._handler._setState(path, k, v); + }, + + // + // see nsIHttpServer.getSharedState + // + getSharedState(k) { + return this._handler._getSharedState(k); + }, + + // + // see nsIHttpServer.setSharedState + // + setSharedState(k, v) { + return this._handler._setSharedState(k, v); + }, + + // + // see nsIHttpServer.getObjectState + // + getObjectState(k) { + return this._handler._getObjectState(k); + }, + + // + // see nsIHttpServer.setObjectState + // + setObjectState(k, v) { + return this._handler._setObjectState(k, v); + }, + + get wrappedJSObject() { + return this; + }, + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: ChromeUtils.generateQI([ + "nsIHttpServer", + "nsIServerSocketListener", + ]), + + // NON-XPCOM PUBLIC API + + /** + * Returns true iff this server is not running (and is not in the process of + * serving any requests still to be processed when the server was last + * stopped after being run). + */ + isStopped() { + return this._socketClosed && !this._hasOpenConnections(); + }, + + // PRIVATE IMPLEMENTATION + + /** True if this server has any open connections to it, false otherwise. */ + _hasOpenConnections() { + // + // If we have any open connections, they're tracked as numeric properties on + // |this._connections|. The non-standard __count__ property could be used + // to check whether there are any properties, but standard-wise, even + // looking forward to ES5, there's no less ugly yet still O(1) way to do + // this. + // + for (var n in this._connections) { + return true; + } + return false; + }, + + /** Calls the server-stopped callback provided when stop() was called. */ + _notifyStopped() { + NS_ASSERT(this._stopCallback !== null, "double-notifying?"); + NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now"); + + // + // NB: We have to grab this now, null out the member, *then* call the + // callback here, or otherwise the callback could (indirectly) futz with + // this._stopCallback by starting and immediately stopping this, at + // which point we'd be nulling out a field we no longer have a right to + // modify. + // + var callback = this._stopCallback; + this._stopCallback = null; + try { + callback(); + } catch (e) { + // not throwing because this is specified as being usually (but not + // always) asynchronous + dump("!!! error running onStopped callback: " + e + "\n"); + } + }, + + /** + * Notifies this server that the given connection has been closed. + * + * @param connection : Connection + * the connection that was closed + */ + _connectionClosed(connection) { + NS_ASSERT( + connection.number in this._connections, + "closing a connection " + + this + + " that we never added to the " + + "set of open connections?" + ); + NS_ASSERT( + this._connections[connection.number] === connection, + "connection number mismatch? " + this._connections[connection.number] + ); + delete this._connections[connection.number]; + + // Fire a pending server-stopped notification if it's our responsibility. + if (!this._hasOpenConnections() && this._socketClosed) { + this._notifyStopped(); + } + }, + + /** + * Requests that the server be shut down when possible. + */ + _requestQuit() { + dumpn(">>> requesting a quit"); + dumpStack(); + this._doQuit = true; + }, +}; + +var HttpServer = nsHttpServer; + +class NodeServer { + // Executes command in the context of a node server. + // See handler in moz-http2.js + // + // Example use: + // let id = NodeServer.fork(); // id is a random string + // await NodeServer.execute(id, `"hello"`) + // > "hello" + // await NodeServer.execute(id, `(() => "hello")()`) + // > "hello" + // await NodeServer.execute(id, `(() => var_defined_on_server)()`) + // > "0" + // await NodeServer.execute(id, `var_defined_on_server`) + // > "0" + // function f(param) { if (param) return param; return "bla"; } + // await NodeServer.execute(id, f); // Defines the function on the server + // await NodeServer.execute(id, `f()`) // executes defined function + // > "bla" + // let result = await NodeServer.execute(id, `f("test")`); + // > "test" + // await NodeServer.kill(id); // shuts down the server + + // Forks a new node server using moz-http2-child.js as a starting point + static fork() { + return this.sendCommand("", "/fork"); + } + // Executes command in the context of the node server indicated by `id` + static execute(id, command) { + return this.sendCommand(command, `/execute/${id}`); + } + // Shuts down the server + static kill(id) { + return this.sendCommand("", `/kill/${id}`); + } + + // Issues a request to the node server (handler defined in moz-http2.js) + // This method should not be called directly. + static sendCommand(command, path) { + let h2Port = Services.env.get("MOZNODE_EXEC_PORT"); + if (!h2Port) { + throw new Error("Could not find MOZNODE_EXEC_PORT"); + } + + let req = new XMLHttpRequest(); + const serverIP = + AppConstants.platform == "android" ? "10.0.2.2" : "127.0.0.1"; + req.open("POST", `http://${serverIP}:${h2Port}${path}`); + req.channel.QueryInterface(Ci.nsIHttpChannelInternal).bypassProxy = true; + + // Passing a function to NodeServer.execute will define that function + // in node. It can be called in a later execute command. + let isFunction = function (obj) { + return !!(obj && obj.constructor && obj.call && obj.apply); + }; + let payload = command; + if (isFunction(command)) { + payload = `${command.name} = ${command.toString()};`; + } + + return new Promise((resolve, reject) => { + req.onload = () => { + let x = null; + + if (req.statusText != "OK") { + reject(`XHR request failed: ${req.statusText}`); + return; + } + + try { + x = JSON.parse(req.responseText); + } catch (e) { + reject(`Failed to parse ${req.responseText} - ${e}`); + return; + } + + if (x.error) { + let e = new Error(x.error, "", 0); + e.stack = x.errorStack; + reject(e); + return; + } + resolve(x.result); + }; + req.onerror = e => { + reject(e); + }; + + req.send(payload.toString()); + }); + } +} + +// +// RFC 2396 section 3.2.2: +// +// host = hostname | IPv4address +// hostname = *( domainlabel "." ) toplabel [ "." ] +// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum +// toplabel = alpha | alpha *( alphanum | "-" ) alphanum +// IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit +// +// IPv6 addresses are notably lacking in the above definition of 'host'. +// RFC 2732 section 3 extends the host definition: +// host = hostname | IPv4address | IPv6reference +// ipv6reference = "[" IPv6address "]" +// +// RFC 3986 supersedes RFC 2732 and offers a more precise definition of a IPv6 +// address. For simplicity, the regexp below captures all canonical IPv6 +// addresses (e.g. [::1]), but may also match valid non-canonical IPv6 addresses +// (e.g. [::127.0.0.1]) and even invalid bracketed addresses ([::], [99999::]). + +const HOST_REGEX = new RegExp( + "^(?:" + + // *( domainlabel "." ) + "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" + + // toplabel [ "." ] + "[a-z](?:[a-z0-9-]*[a-z0-9])?\\.?" + + "|" + + // IPv4 address + "\\d+\\.\\d+\\.\\d+\\.\\d+" + + "|" + + // IPv6 addresses (e.g. [::1]) + "\\[[:0-9a-f]+\\]" + + ")$", + "i" +); + +/** + * Represents the identity of a server. An identity consists of a set of + * (scheme, host, port) tuples denoted as locations (allowing a single server to + * serve multiple sites or to be used behind both HTTP and HTTPS proxies for any + * host/port). Any incoming request must be to one of these locations, or it + * will be rejected with an HTTP 400 error. One location, denoted as the + * primary location, is the location assigned in contexts where a location + * cannot otherwise be endogenously derived, such as for HTTP/1.0 requests. + * + * A single identity may contain at most one location per unique host/port pair; + * other than that, no restrictions are placed upon what locations may + * constitute an identity. + */ +function ServerIdentity() { + /** The scheme of the primary location. */ + this._primaryScheme = "http"; + + /** The hostname of the primary location. */ + this._primaryHost = "127.0.0.1"; + + /** The port number of the primary location. */ + this._primaryPort = -1; + + /** + * The current port number for the corresponding server, stored so that a new + * primary location can always be set if the current one is removed. + */ + this._defaultPort = -1; + + /** + * Maps hosts to maps of ports to schemes, e.g. the following would represent + * https://example.com:789/ and http://example.org/: + * + * { + * "xexample.com": { 789: "https" }, + * "xexample.org": { 80: "http" } + * } + * + * Note the "x" prefix on hostnames, which prevents collisions with special + * JS names like "prototype". + */ + this._locations = { xlocalhost: {} }; +} +ServerIdentity.prototype = { + // NSIHTTPSERVERIDENTITY + + // + // see nsIHttpServerIdentity.primaryScheme + // + get primaryScheme() { + if (this._primaryPort === -1) { + throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED); + } + return this._primaryScheme; + }, + + // + // see nsIHttpServerIdentity.primaryHost + // + get primaryHost() { + if (this._primaryPort === -1) { + throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED); + } + return this._primaryHost; + }, + + // + // see nsIHttpServerIdentity.primaryPort + // + get primaryPort() { + if (this._primaryPort === -1) { + throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED); + } + return this._primaryPort; + }, + + // + // see nsIHttpServerIdentity.add + // + add(scheme, host, port) { + this._validate(scheme, host, port); + + var entry = this._locations["x" + host]; + if (!entry) { + this._locations["x" + host] = entry = {}; + } + + entry[port] = scheme; + }, + + // + // see nsIHttpServerIdentity.remove + // + remove(scheme, host, port) { + this._validate(scheme, host, port); + + var entry = this._locations["x" + host]; + if (!entry) { + return false; + } + + var present = port in entry; + delete entry[port]; + + if ( + this._primaryScheme == scheme && + this._primaryHost == host && + this._primaryPort == port && + this._defaultPort !== -1 + ) { + // Always keep at least one identity in existence at any time, unless + // we're in the process of shutting down (the last condition above). + this._primaryPort = -1; + this._initialize(this._defaultPort, host, false); + } + + return present; + }, + + // + // see nsIHttpServerIdentity.has + // + has(scheme, host, port) { + this._validate(scheme, host, port); + + return ( + "x" + host in this._locations && + scheme === this._locations["x" + host][port] + ); + }, + + // + // see nsIHttpServerIdentity.has + // + getScheme(host, port) { + this._validate("http", host, port); + + var entry = this._locations["x" + host]; + if (!entry) { + return ""; + } + + return entry[port] || ""; + }, + + // + // see nsIHttpServerIdentity.setPrimary + // + setPrimary(scheme, host, port) { + this._validate(scheme, host, port); + + this.add(scheme, host, port); + + this._primaryScheme = scheme; + this._primaryHost = host; + this._primaryPort = port; + }, + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: ChromeUtils.generateQI(["nsIHttpServerIdentity"]), + + // PRIVATE IMPLEMENTATION + + /** + * Initializes the primary name for the corresponding server, based on the + * provided port number. + */ + _initialize(port, host, addSecondaryDefault, dualStack) { + this._host = host; + if (this._primaryPort !== -1) { + this.add("http", host, port); + } else { + this.setPrimary("http", "localhost", port); + } + this._defaultPort = port; + + // Only add this if we're being called at server startup + if (addSecondaryDefault && host != "127.0.0.1") { + if (host.includes(":")) { + this.add("http", "[::1]", port); + if (dualStack) { + this.add("http", "127.0.0.1", port); + } + } else { + this.add("http", "127.0.0.1", port); + } + } + }, + + /** + * Called at server shutdown time, unsets the primary location only if it was + * the default-assigned location and removes the default location from the + * set of locations used. + */ + _teardown() { + if (this._host != "127.0.0.1") { + // Not the default primary location, nothing special to do here + this.remove("http", "127.0.0.1", this._defaultPort); + } + + // This is a *very* tricky bit of reasoning here; make absolutely sure the + // tests for this code pass before you commit changes to it. + if ( + this._primaryScheme == "http" && + this._primaryHost == this._host && + this._primaryPort == this._defaultPort + ) { + // Make sure we don't trigger the readding logic in .remove(), then remove + // the default location. + var port = this._defaultPort; + this._defaultPort = -1; + this.remove("http", this._host, port); + + // Ensure a server start triggers the setPrimary() path in ._initialize() + this._primaryPort = -1; + } else { + // No reason not to remove directly as it's not our primary location + this.remove("http", this._host, this._defaultPort); + } + }, + + /** + * Ensures scheme, host, and port are all valid with respect to RFC 2396. + * + * @throws NS_ERROR_ILLEGAL_VALUE + * if any argument doesn't match the corresponding production + */ + _validate(scheme, host, port) { + if (scheme !== "http" && scheme !== "https") { + dumpn("*** server only supports http/https schemes: '" + scheme + "'"); + dumpStack(); + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + if (!HOST_REGEX.test(host)) { + dumpn("*** unexpected host: '" + host + "'"); + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + if (port < 0 || port > 65535) { + dumpn("*** unexpected port: '" + port + "'"); + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + }, +}; + +/** + * Represents a connection to the server (and possibly in the future the thread + * on which the connection is processed). + * + * @param input : nsIInputStream + * stream from which incoming data on the connection is read + * @param output : nsIOutputStream + * stream to write data out the connection + * @param server : nsHttpServer + * the server handling the connection + * @param port : int + * the port on which the server is running + * @param outgoingPort : int + * the outgoing port used by this connection + * @param number : uint + * a serial number used to uniquely identify this connection + */ +function Connection( + input, + output, + server, + port, + outgoingPort, + number, + transport +) { + dumpn("*** opening new connection " + number + " on port " + outgoingPort); + + /** Stream of incoming data. */ + this.input = input; + + /** Stream for outgoing data. */ + this.output = output; + + /** The server associated with this request. */ + this.server = server; + + /** The port on which the server is running. */ + this.port = port; + + /** The outgoing poort used by this connection. */ + this._outgoingPort = outgoingPort; + + /** The serial number of this connection. */ + this.number = number; + + /** Reference to the underlying transport. */ + this.transport = transport; + + /** + * The request for which a response is being generated, null if the + * incoming request has not been fully received or if it had errors. + */ + this.request = null; + + /** This allows a connection to disambiguate between a peer initiating a + * close and the socket being forced closed on shutdown. + */ + this._closed = false; + + /** State variable for debugging. */ + this._processed = false; + + /** whether or not 1st line of request has been received */ + this._requestStarted = false; +} +Connection.prototype = { + /** Closes this connection's input/output streams. */ + close() { + if (this._closed) { + return; + } + + dumpn( + "*** closing connection " + this.number + " on port " + this._outgoingPort + ); + + this.input.close(); + this.output.close(); + this._closed = true; + + var server = this.server; + server._connectionClosed(this); + + // If an error triggered a server shutdown, act on it now + if (server._doQuit) { + server.stop(function () { + /* not like we can do anything better */ + }); + } + }, + + /** + * Initiates processing of this connection, using the data in the given + * request. + * + * @param request : Request + * the request which should be processed + */ + process(request) { + NS_ASSERT(!this._closed && !this._processed); + + this._processed = true; + + this.request = request; + this.server._handler.handleResponse(this); + }, + + /** + * Initiates processing of this connection, generating a response with the + * given HTTP error code. + * + * @param code : uint + * an HTTP code, so in the range [0, 1000) + * @param request : Request + * incomplete data about the incoming request (since there were errors + * during its processing + */ + processError(code, request) { + NS_ASSERT(!this._closed && !this._processed); + + this._processed = true; + this.request = request; + this.server._handler.handleError(code, this); + }, + + /** Converts this to a string for debugging purposes. */ + toString() { + return ( + "<Connection(" + + this.number + + (this.request ? ", " + this.request.path : "") + + "): " + + (this._closed ? "closed" : "open") + + ">" + ); + }, + + requestStarted() { + this._requestStarted = true; + }, +}; + +/** Returns an array of count bytes from the given input stream. */ +function readBytes(inputStream, count) { + return new BinaryInputStream(inputStream).readByteArray(count); +} + +/** Request reader processing states; see RequestReader for details. */ +const READER_IN_REQUEST_LINE = 0; +const READER_IN_HEADERS = 1; +const READER_IN_BODY = 2; +const READER_FINISHED = 3; + +/** + * Reads incoming request data asynchronously, does any necessary preprocessing, + * and forwards it to the request handler. Processing occurs in three states: + * + * READER_IN_REQUEST_LINE Reading the request's status line + * READER_IN_HEADERS Reading headers in the request + * READER_IN_BODY Reading the body of the request + * READER_FINISHED Entire request has been read and processed + * + * During the first two stages, initial metadata about the request is gathered + * into a Request object. Once the status line and headers have been processed, + * we start processing the body of the request into the Request. Finally, when + * the entire body has been read, we create a Response and hand it off to the + * ServerHandler to be given to the appropriate request handler. + * + * @param connection : Connection + * the connection for the request being read + */ +function RequestReader(connection) { + /** Connection metadata for this request. */ + this._connection = connection; + + /** + * A container providing line-by-line access to the raw bytes that make up the + * data which has been read from the connection but has not yet been acted + * upon (by passing it to the request handler or by extracting request + * metadata from it). + */ + this._data = new LineData(); + + /** + * The amount of data remaining to be read from the body of this request. + * After all headers in the request have been read this is the value in the + * Content-Length header, but as the body is read its value decreases to zero. + */ + this._contentLength = 0; + + /** The current state of parsing the incoming request. */ + this._state = READER_IN_REQUEST_LINE; + + /** Metadata constructed from the incoming request for the request handler. */ + this._metadata = new Request(connection.port); + + /** + * Used to preserve state if we run out of line data midway through a + * multi-line header. _lastHeaderName stores the name of the header, while + * _lastHeaderValue stores the value we've seen so far for the header. + * + * These fields are always either both undefined or both strings. + */ + this._lastHeaderName = this._lastHeaderValue = undefined; +} +RequestReader.prototype = { + // NSIINPUTSTREAMCALLBACK + + /** + * Called when more data from the incoming request is available. This method + * then reads the available data from input and deals with that data as + * necessary, depending upon the syntax of already-downloaded data. + * + * @param input : nsIAsyncInputStream + * the stream of incoming data from the connection + */ + onInputStreamReady(input) { + dumpn( + "*** onInputStreamReady(input=" + + input + + ") on thread " + + gThreadManager.currentThread + + " (main is " + + gThreadManager.mainThread + + ")" + ); + dumpn("*** this._state == " + this._state); + + // Handle cases where we get more data after a request error has been + // discovered but *before* we can close the connection. + var data = this._data; + if (!data) { + return; + } + + try { + data.appendBytes(readBytes(input, input.available())); + } catch (e) { + if (streamClosed(e)) { + dumpn( + "*** WARNING: unexpected error when reading from socket; will " + + "be treated as if the input stream had been closed" + ); + dumpn("*** WARNING: actual error was: " + e); + } + + // We've lost a race -- input has been closed, but we're still expecting + // to read more data. available() will throw in this case, and since + // we're dead in the water now, destroy the connection. + dumpn( + "*** onInputStreamReady called on a closed input, destroying " + + "connection" + ); + this._connection.close(); + return; + } + + switch (this._state) { + default: + NS_ASSERT(false, "invalid state: " + this._state); + break; + + case READER_IN_REQUEST_LINE: + if (!this._processRequestLine()) { + break; + } + /* fall through */ + + case READER_IN_HEADERS: + if (!this._processHeaders()) { + break; + } + /* fall through */ + + case READER_IN_BODY: + this._processBody(); + } + + if (this._state != READER_FINISHED) { + input.asyncWait(this, 0, 0, gThreadManager.currentThread); + } + }, + + // + // see nsISupports.QueryInterface + // + QueryInterface: ChromeUtils.generateQI(["nsIInputStreamCallback"]), + + // PRIVATE API + + /** + * Processes unprocessed, downloaded data as a request line. + * + * @returns boolean + * true iff the request line has been fully processed + */ + _processRequestLine() { + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); + + // Servers SHOULD ignore any empty line(s) received where a Request-Line + // is expected (section 4.1). + var data = this._data; + var line = {}; + var readSuccess; + while ((readSuccess = data.readLine(line)) && line.value == "") { + dumpn("*** ignoring beginning blank line..."); + } + + // if we don't have a full line, wait until we do + if (!readSuccess) { + return false; + } + + // we have the first non-blank line + try { + this._parseRequestLine(line.value); + this._state = READER_IN_HEADERS; + this._connection.requestStarted(); + return true; + } catch (e) { + this._handleError(e); + return false; + } + }, + + /** + * Processes stored data, assuming it is either at the beginning or in + * the middle of processing request headers. + * + * @returns boolean + * true iff header data in the request has been fully processed + */ + _processHeaders() { + NS_ASSERT(this._state == READER_IN_HEADERS); + + // XXX things to fix here: + // + // - need to support RFC 2047-encoded non-US-ASCII characters + + try { + var done = this._parseHeaders(); + if (done) { + var request = this._metadata; + + // XXX this is wrong for requests with transfer-encodings applied to + // them, particularly chunked (which by its nature can have no + // meaningful Content-Length header)! + this._contentLength = request.hasHeader("Content-Length") + ? parseInt(request.getHeader("Content-Length"), 10) + : 0; + dumpn("_processHeaders, Content-length=" + this._contentLength); + + this._state = READER_IN_BODY; + } + return done; + } catch (e) { + this._handleError(e); + return false; + } + }, + + /** + * Processes stored data, assuming it is either at the beginning or in + * the middle of processing the request body. + * + * @returns boolean + * true iff the request body has been fully processed + */ + _processBody() { + NS_ASSERT(this._state == READER_IN_BODY); + + // XXX handle chunked transfer-coding request bodies! + + try { + if (this._contentLength > 0) { + var data = this._data.purge(); + var count = Math.min(data.length, this._contentLength); + dumpn( + "*** loading data=" + + data + + " len=" + + data.length + + " excess=" + + (data.length - count) + ); + data.length = count; + + var bos = new BinaryOutputStream(this._metadata._bodyOutputStream); + bos.writeByteArray(data); + this._contentLength -= count; + } + + dumpn("*** remaining body data len=" + this._contentLength); + if (this._contentLength == 0) { + this._validateRequest(); + this._state = READER_FINISHED; + this._handleResponse(); + return true; + } + + return false; + } catch (e) { + this._handleError(e); + return false; + } + }, + + /** + * Does various post-header checks on the data in this request. + * + * @throws : HttpError + * if the request was malformed in some way + */ + _validateRequest() { + NS_ASSERT(this._state == READER_IN_BODY); + + dumpn("*** _validateRequest"); + + var metadata = this._metadata; + var headers = metadata._headers; + + // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header + var identity = this._connection.server.identity; + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) { + if (!headers.hasHeader("Host")) { + dumpn("*** malformed HTTP/1.1 or greater request with no Host header!"); + throw HTTP_400; + } + + // If the Request-URI wasn't absolute, then we need to determine our host. + // We have to determine what scheme was used to access us based on the + // server identity data at this point, because the request just doesn't + // contain enough data on its own to do this, sadly. + if (!metadata._host) { + var host, port; + var hostPort = headers.getHeader("Host"); + var colon = hostPort.lastIndexOf(":"); + if (hostPort.lastIndexOf("]") > colon) { + colon = -1; + } + if (colon < 0) { + host = hostPort; + port = ""; + } else { + host = hostPort.substring(0, colon); + port = hostPort.substring(colon + 1); + } + + // NB: We allow an empty port here because, oddly, a colon may be + // present even without a port number, e.g. "example.com:"; in this + // case the default port applies. + if (!HOST_REGEX.test(host) || !/^\d*$/.test(port)) { + dumpn( + "*** malformed hostname (" + + hostPort + + ") in Host " + + "header, 400 time" + ); + throw HTTP_400; + } + + // If we're not given a port, we're stuck, because we don't know what + // scheme to use to look up the correct port here, in general. Since + // the HTTPS case requires a tunnel/proxy and thus requires that the + // requested URI be absolute (and thus contain the necessary + // information), let's assume HTTP will prevail and use that. + port = +port || 80; + + var scheme = identity.getScheme(host, port); + if (!scheme) { + dumpn( + "*** unrecognized hostname (" + + hostPort + + ") in Host " + + "header, 400 time" + ); + throw HTTP_400; + } + + metadata._scheme = scheme; + metadata._host = host; + metadata._port = port; + } + } else { + NS_ASSERT( + metadata._host === undefined, + "HTTP/1.0 doesn't allow absolute paths in the request line!" + ); + + metadata._scheme = identity.primaryScheme; + metadata._host = identity.primaryHost; + metadata._port = identity.primaryPort; + } + + NS_ASSERT( + identity.has(metadata._scheme, metadata._host, metadata._port), + "must have a location we recognize by now!" + ); + }, + + /** + * Handles responses in case of error, either in the server or in the request. + * + * @param e + * the specific error encountered, which is an HttpError in the case where + * the request is in some way invalid or cannot be fulfilled; if this isn't + * an HttpError we're going to be paranoid and shut down, because that + * shouldn't happen, ever + */ + _handleError(e) { + // Don't fall back into normal processing! + this._state = READER_FINISHED; + + var server = this._connection.server; + if (e instanceof HttpError) { + var code = e.code; + } else { + dumpn( + "!!! UNEXPECTED ERROR: " + + e + + (e.lineNumber ? ", line " + e.lineNumber : "") + ); + + // no idea what happened -- be paranoid and shut down + code = 500; + server._requestQuit(); + } + + // make attempted reuse of data an error + this._data = null; + + this._connection.processError(code, this._metadata); + }, + + /** + * Now that we've read the request line and headers, we can actually hand off + * the request to be handled. + * + * This method is called once per request, after the request line and all + * headers and the body, if any, have been received. + */ + _handleResponse() { + NS_ASSERT(this._state == READER_FINISHED); + + // We don't need the line-based data any more, so make attempted reuse an + // error. + this._data = null; + + this._connection.process(this._metadata); + }, + + // PARSING + + /** + * Parses the request line for the HTTP request associated with this. + * + * @param line : string + * the request line + */ + _parseRequestLine(line) { + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); + + dumpn("*** _parseRequestLine('" + line + "')"); + + var metadata = this._metadata; + + // clients and servers SHOULD accept any amount of SP or HT characters + // between fields, even though only a single SP is required (section 19.3) + var request = line.split(/[ \t]+/); + if (!request || request.length != 3) { + dumpn("*** No request in line"); + throw HTTP_400; + } + + metadata._method = request[0]; + + // get the HTTP version + var ver = request[2]; + var match = ver.match(/^HTTP\/(\d+\.\d+)$/); + if (!match) { + dumpn("*** No HTTP version in line"); + throw HTTP_400; + } + + // determine HTTP version + try { + metadata._httpVersion = new nsHttpVersion(match[1]); + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0)) { + throw new Error("unsupported HTTP version"); + } + } catch (e) { + // we support HTTP/1.0 and HTTP/1.1 only + throw HTTP_501; + } + + var fullPath = request[1]; + + if (metadata._method == "CONNECT") { + metadata._path = "CONNECT"; + metadata._scheme = "https"; + [metadata._host, metadata._port] = fullPath.split(":"); + return; + } + + var serverIdentity = this._connection.server.identity; + var scheme, host, port; + + if (fullPath.charAt(0) != "/") { + // No absolute paths in the request line in HTTP prior to 1.1 + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) { + dumpn("*** Metadata version too low"); + throw HTTP_400; + } + + try { + var uri = Services.io.newURI(fullPath); + fullPath = uri.pathQueryRef; + scheme = uri.scheme; + host = uri.asciiHost; + if (host.includes(":")) { + // If the host still contains a ":", then it is an IPv6 address. + // IPv6 addresses-as-host are registered with brackets, so we need to + // wrap the host in brackets because nsIURI's host lacks them. + // This inconsistency in nsStandardURL is tracked at bug 1195459. + host = `[${host}]`; + } + metadata._host = host; + port = uri.port; + if (port === -1) { + if (scheme === "http") { + port = 80; + } else if (scheme === "https") { + port = 443; + } else { + dumpn("*** Unknown scheme: " + scheme); + throw HTTP_400; + } + } + } catch (e) { + // If the host is not a valid host on the server, the response MUST be a + // 400 (Bad Request) error message (section 5.2). Alternately, the URI + // is malformed. + dumpn("*** Threw when dealing with URI: " + e); + throw HTTP_400; + } + + if ( + !serverIdentity.has(scheme, host, port) || + fullPath.charAt(0) != "/" + ) { + dumpn("*** serverIdentity unknown or path does not start with '/'"); + throw HTTP_400; + } + } + + var splitter = fullPath.indexOf("?"); + if (splitter < 0) { + // _queryString already set in ctor + metadata._path = fullPath; + } else { + metadata._path = fullPath.substring(0, splitter); + metadata._queryString = fullPath.substring(splitter + 1); + } + + metadata._scheme = scheme; + metadata._host = host; + metadata._port = port; + }, + + /** + * Parses all available HTTP headers in this until the header-ending CRLFCRLF, + * adding them to the store of headers in the request. + * + * @throws + * HTTP_400 if the headers are malformed + * @returns boolean + * true if all headers have now been processed, false otherwise + */ + _parseHeaders() { + NS_ASSERT(this._state == READER_IN_HEADERS); + + dumpn("*** _parseHeaders"); + + var data = this._data; + + var headers = this._metadata._headers; + var lastName = this._lastHeaderName; + var lastVal = this._lastHeaderValue; + + var line = {}; + while (true) { + dumpn("*** Last name: '" + lastName + "'"); + dumpn("*** Last val: '" + lastVal + "'"); + NS_ASSERT( + !((lastVal === undefined) ^ (lastName === undefined)), + lastName === undefined + ? "lastVal without lastName? lastVal: '" + lastVal + "'" + : "lastName without lastVal? lastName: '" + lastName + "'" + ); + + if (!data.readLine(line)) { + // save any data we have from the header we might still be processing + this._lastHeaderName = lastName; + this._lastHeaderValue = lastVal; + return false; + } + + var lineText = line.value; + dumpn("*** Line text: '" + lineText + "'"); + var firstChar = lineText.charAt(0); + + // blank line means end of headers + if (lineText == "") { + // we're finished with the previous header + if (lastName) { + try { + headers.setHeader(lastName, lastVal, true); + } catch (e) { + dumpn("*** setHeader threw on last header, e == " + e); + throw HTTP_400; + } + } else { + // no headers in request -- valid for HTTP/1.0 requests + } + + // either way, we're done processing headers + this._state = READER_IN_BODY; + return true; + } else if (firstChar == " " || firstChar == "\t") { + // multi-line header if we've already seen a header line + if (!lastName) { + dumpn("We don't have a header to continue!"); + throw HTTP_400; + } + + // append this line's text to the value; starts with SP/HT, so no need + // for separating whitespace + lastVal += lineText; + } else { + // we have a new header, so set the old one (if one existed) + if (lastName) { + try { + headers.setHeader(lastName, lastVal, true); + } catch (e) { + dumpn("*** setHeader threw on a header, e == " + e); + throw HTTP_400; + } + } + + var colon = lineText.indexOf(":"); // first colon must be splitter + if (colon < 1) { + dumpn("*** No colon or missing header field-name"); + throw HTTP_400; + } + + // set header name, value (to be set in the next loop, usually) + lastName = lineText.substring(0, colon); + lastVal = lineText.substring(colon + 1); + } // empty, continuation, start of header + } // while (true) + }, +}; + +/** The character codes for CR and LF. */ +const CR = 0x0d, + LF = 0x0a; + +/** + * Calculates the number of characters before the first CRLF pair in array, or + * -1 if the array contains no CRLF pair. + * + * @param array : Array + * an array of numbers in the range [0, 256), each representing a single + * character; the first CRLF is the lowest index i where + * |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|, + * if such an |i| exists, and -1 otherwise + * @param start : uint + * start index from which to begin searching in array + * @returns int + * the index of the first CRLF if any were present, -1 otherwise + */ +function findCRLF(array, start) { + for (var i = array.indexOf(CR, start); i >= 0; i = array.indexOf(CR, i + 1)) { + if (array[i + 1] == LF) { + return i; + } + } + return -1; +} + +/** + * A container which provides line-by-line access to the arrays of bytes with + * which it is seeded. + */ +function LineData() { + /** An array of queued bytes from which to get line-based characters. */ + this._data = []; + + /** Start index from which to search for CRLF. */ + this._start = 0; +} +LineData.prototype = { + /** + * Appends the bytes in the given array to the internal data cache maintained + * by this. + */ + appendBytes(bytes) { + var count = bytes.length; + var quantum = 262144; // just above half SpiderMonkey's argument-count limit + if (count < quantum) { + Array.prototype.push.apply(this._data, bytes); + return; + } + + // Large numbers of bytes may cause Array.prototype.push to be called with + // more arguments than the JavaScript engine supports. In that case append + // bytes in fixed-size amounts until all bytes are appended. + for (var start = 0; start < count; start += quantum) { + var slice = bytes.slice(start, Math.min(start + quantum, count)); + Array.prototype.push.apply(this._data, slice); + } + }, + + /** + * Removes and returns a line of data, delimited by CRLF, from this. + * + * @param out + * an object whose "value" property will be set to the first line of text + * present in this, sans CRLF, if this contains a full CRLF-delimited line + * of text; if this doesn't contain enough data, the value of the property + * is undefined + * @returns boolean + * true if a full line of data could be read from the data in this, false + * otherwise + */ + readLine(out) { + var data = this._data; + var length = findCRLF(data, this._start); + if (length < 0) { + this._start = data.length; + + // But if our data ends in a CR, we have to back up one, because + // the first byte in the next packet might be an LF and if we + // start looking at data.length we won't find it. + if (data.length && data[data.length - 1] === CR) { + --this._start; + } + + return false; + } + + // Reset for future lines. + this._start = 0; + + // + // We have the index of the CR, so remove all the characters, including + // CRLF, from the array with splice, and convert the removed array + // (excluding the trailing CRLF characters) into the corresponding string. + // + var leading = data.splice(0, length + 2); + var quantum = 262144; + var line = ""; + for (var start = 0; start < length; start += quantum) { + var slice = leading.slice(start, Math.min(start + quantum, length)); + line += String.fromCharCode.apply(null, slice); + } + + out.value = line; + return true; + }, + + /** + * Removes the bytes currently within this and returns them in an array. + * + * @returns Array + * the bytes within this when this method is called + */ + purge() { + var data = this._data; + this._data = []; + return data; + }, +}; + +/** + * Creates a request-handling function for an nsIHttpRequestHandler object. + */ +function createHandlerFunc(handler) { + return function (metadata, response) { + handler.handle(metadata, response); + }; +} + +/** + * The default handler for directories; writes an HTML response containing a + * slightly-formatted directory listing. + */ +function defaultIndexHandler(metadata, response) { + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var path = htmlEscape(decodeURI(metadata.path)); + + // + // Just do a very basic bit of directory listings -- no need for too much + // fanciness, especially since we don't have a style sheet in which we can + // stick rules (don't want to pollute the default path-space). + // + + var body = + "<html>\ + <head>\ + <title>" + + path + + "</title>\ + </head>\ + <body>\ + <h1>" + + path + + '</h1>\ + <ol style="list-style-type: none">'; + + var directory = metadata.getProperty("directory"); + NS_ASSERT(directory && directory.isDirectory()); + + var fileList = []; + var files = directory.directoryEntries; + while (files.hasMoreElements()) { + var f = files.nextFile; + let name = f.leafName; + if ( + !f.isHidden() && + (name.charAt(name.length - 1) != HIDDEN_CHAR || + name.charAt(name.length - 2) == HIDDEN_CHAR) + ) { + fileList.push(f); + } + } + + fileList.sort(fileSort); + + for (var i = 0; i < fileList.length; i++) { + var file = fileList[i]; + try { + let name = file.leafName; + if (name.charAt(name.length - 1) == HIDDEN_CHAR) { + name = name.substring(0, name.length - 1); + } + var sep = file.isDirectory() ? "/" : ""; + + // Note: using " to delimit the attribute here because encodeURIComponent + // passes through '. + var item = + '<li><a href="' + + encodeURIComponent(name) + + sep + + '">' + + htmlEscape(name) + + sep + + "</a></li>"; + + body += item; + } catch (e) { + /* some file system error, ignore the file */ + } + } + + body += + " </ol>\ + </body>\ + </html>"; + + response.bodyOutputStream.write(body, body.length); +} + +/** + * Sorts a and b (nsIFile objects) into an aesthetically pleasing order. + */ +function fileSort(a, b) { + var dira = a.isDirectory(), + dirb = b.isDirectory(); + + if (dira && !dirb) { + return -1; + } + if (dirb && !dira) { + return 1; + } + + var namea = a.leafName.toLowerCase(), + nameb = b.leafName.toLowerCase(); + return nameb > namea ? -1 : 1; +} + +/** + * Converts an externally-provided path into an internal path for use in + * determining file mappings. + * + * @param path + * the path to convert + * @param encoded + * true if the given path should be passed through decodeURI prior to + * conversion + * @throws URIError + * if path is incorrectly encoded + */ +function toInternalPath(path, encoded) { + if (encoded) { + path = decodeURI(path); + } + + var comps = path.split("/"); + for (var i = 0, sz = comps.length; i < sz; i++) { + var comp = comps[i]; + if (comp.charAt(comp.length - 1) == HIDDEN_CHAR) { + comps[i] = comp + HIDDEN_CHAR; + } + } + return comps.join("/"); +} + +const PERMS_READONLY = (4 << 6) | (4 << 3) | 4; + +/** + * Adds custom-specified headers for the given file to the given response, if + * any such headers are specified. + * + * @param file + * the file on the disk which is to be written + * @param metadata + * metadata about the incoming request + * @param response + * the Response to which any specified headers/data should be written + * @throws HTTP_500 + * if an error occurred while processing custom-specified headers + */ +function maybeAddHeadersInternal( + file, + metadata, + response, + informationalResponse +) { + var name = file.leafName; + if (name.charAt(name.length - 1) == HIDDEN_CHAR) { + name = name.substring(0, name.length - 1); + } + + var headerFile = file.parent; + if (!informationalResponse) { + headerFile.append(name + HEADERS_SUFFIX); + } else { + headerFile.append(name + INFORMATIONAL_RESPONSE_SUFFIX); + } + + if (!headerFile.exists()) { + return; + } + + const PR_RDONLY = 0x01; + var fis = new FileInputStream( + headerFile, + PR_RDONLY, + PERMS_READONLY, + Ci.nsIFileInputStream.CLOSE_ON_EOF + ); + + try { + var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); + lis.QueryInterface(Ci.nsIUnicharLineInputStream); + + var line = { value: "" }; + var more = lis.readLine(line); + + if (!more && line.value == "") { + return; + } + + // request line + + var status = line.value; + if (status.indexOf("HTTP ") == 0) { + status = status.substring(5); + var space = status.indexOf(" "); + var code, description; + if (space < 0) { + code = status; + description = ""; + } else { + code = status.substring(0, space); + description = status.substring(space + 1, status.length); + } + + if (!informationalResponse) { + response.setStatusLine( + metadata.httpVersion, + parseInt(code, 10), + description + ); + } else { + response.setInformationalResponseStatusLine( + metadata.httpVersion, + parseInt(code, 10), + description + ); + } + + line.value = ""; + more = lis.readLine(line); + } else if (informationalResponse) { + // An informational response must have a status line. + return; + } + + // headers + while (more || line.value != "") { + var header = line.value; + var colon = header.indexOf(":"); + + if (!informationalResponse) { + response.setHeader( + header.substring(0, colon), + header.substring(colon + 1, header.length), + false + ); // allow overriding server-set headers + } else { + response.setInformationalResponseHeader( + header.substring(0, colon), + header.substring(colon + 1, header.length), + false + ); // allow overriding server-set headers + } + + line.value = ""; + more = lis.readLine(line); + } + } catch (e) { + dumpn("WARNING: error in headers for " + metadata.path + ": " + e); + throw HTTP_500; + } finally { + fis.close(); + } +} + +function maybeAddHeaders(file, metadata, response) { + maybeAddHeadersInternal(file, metadata, response, false); +} + +function maybeAddInformationalResponse(file, metadata, response) { + maybeAddHeadersInternal(file, metadata, response, true); +} + +/** + * An object which handles requests for a server, executing default and + * overridden behaviors as instructed by the code which uses and manipulates it. + * Default behavior includes the paths / and /trace (diagnostics), with some + * support for HTTP error pages for various codes and fallback to HTTP 500 if + * those codes fail for any reason. + * + * @param server : nsHttpServer + * the server in which this handler is being used + */ +function ServerHandler(server) { + // FIELDS + + /** + * The nsHttpServer instance associated with this handler. + */ + this._server = server; + + /** + * A FileMap object containing the set of path->nsIFile mappings for + * all directory mappings set in the server (e.g., "/" for /var/www/html/, + * "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2). + * + * Note carefully: the leading and trailing "/" in each path (not file) are + * removed before insertion to simplify the code which uses this. You have + * been warned! + */ + this._pathDirectoryMap = new FileMap(); + + /** + * Custom request handlers for the server in which this resides. Path-handler + * pairs are stored as property-value pairs in this property. + * + * @see ServerHandler.prototype._defaultPaths + */ + this._overridePaths = {}; + + /** + * Custom request handlers for the path prefixes on the server in which this + * resides. Path-handler pairs are stored as property-value pairs in this + * property. + * + * @see ServerHandler.prototype._defaultPaths + */ + this._overridePrefixes = {}; + + /** + * Custom request handlers for the error handlers in the server in which this + * resides. Path-handler pairs are stored as property-value pairs in this + * property. + * + * @see ServerHandler.prototype._defaultErrors + */ + this._overrideErrors = {}; + + /** + * Maps file extensions to their MIME types in the server, overriding any + * mapping that might or might not exist in the MIME service. + */ + this._mimeMappings = {}; + + /** + * The default handler for requests for directories, used to serve directories + * when no index file is present. + */ + this._indexHandler = defaultIndexHandler; + + /** Per-path state storage for the server. */ + this._state = {}; + + /** Entire-server state storage. */ + this._sharedState = {}; + + /** Entire-server state storage for nsISupports values. */ + this._objectState = {}; +} +ServerHandler.prototype = { + // PUBLIC API + + /** + * Handles a request to this server, responding to the request appropriately + * and initiating server shutdown if necessary. + * + * This method never throws an exception. + * + * @param connection : Connection + * the connection for this request + */ + handleResponse(connection) { + var request = connection.request; + var response = new Response(connection); + + var path = request.path; + dumpn("*** path == " + path); + + try { + try { + if (path in this._overridePaths) { + // explicit paths first, then files based on existing directory mappings, + // then (if the file doesn't exist) built-in server default paths + dumpn("calling override for " + path); + this._overridePaths[path](request, response); + } else { + var longestPrefix = ""; + for (let prefix in this._overridePrefixes) { + if ( + prefix.length > longestPrefix.length && + path.substr(0, prefix.length) == prefix + ) { + longestPrefix = prefix; + } + } + if (longestPrefix.length) { + dumpn("calling prefix override for " + longestPrefix); + this._overridePrefixes[longestPrefix](request, response); + } else { + this._handleDefault(request, response); + } + } + } catch (e) { + if (response.partiallySent()) { + response.abort(e); + return; + } + + if (!(e instanceof HttpError)) { + dumpn("*** unexpected error: e == " + e); + throw HTTP_500; + } + if (e.code !== 404) { + throw e; + } + + dumpn("*** default: " + (path in this._defaultPaths)); + + response = new Response(connection); + if (path in this._defaultPaths) { + this._defaultPaths[path](request, response); + } else { + throw HTTP_404; + } + } + } catch (e) { + if (response.partiallySent()) { + response.abort(e); + return; + } + + var errorCode = "internal"; + + try { + if (!(e instanceof HttpError)) { + throw e; + } + + errorCode = e.code; + dumpn("*** errorCode == " + errorCode); + + response = new Response(connection); + if (e.customErrorHandling) { + e.customErrorHandling(response); + } + this._handleError(errorCode, request, response); + return; + } catch (e2) { + dumpn( + "*** error handling " + + errorCode + + " error: " + + "e2 == " + + e2 + + ", shutting down server" + ); + + connection.server._requestQuit(); + response.abort(e2); + return; + } + } + + response.complete(); + }, + + // + // see nsIHttpServer.registerFile + // + registerFile(path, file, handler) { + if (!file) { + dumpn("*** unregistering '" + path + "' mapping"); + delete this._overridePaths[path]; + return; + } + + dumpn("*** registering '" + path + "' as mapping to " + file.path); + file = file.clone(); + + var self = this; + this._overridePaths[path] = function (request, response) { + if (!file.exists()) { + throw HTTP_404; + } + + dumpn("*** responding '" + path + "' as mapping to " + file.path); + + response.setStatusLine(request.httpVersion, 200, "OK"); + if (typeof handler === "function") { + handler(request, response); + } + self._writeFileResponse(request, file, response, 0, file.fileSize); + }; + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler(path, handler) { + if (!path.length) { + throw Components.Exception( + "Handler path cannot be empty", + Cr.NS_ERROR_INVALID_ARG + ); + } + + // XXX true path validation! + if (path.charAt(0) != "/" && path != "CONNECT") { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + this._handlerToField(handler, this._overridePaths, path); + }, + + // + // see nsIHttpServer.registerPrefixHandler + // + registerPrefixHandler(path, handler) { + // XXX true path validation! + if (path.charAt(0) != "/" || path.charAt(path.length - 1) != "/") { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + this._handlerToField(handler, this._overridePrefixes, path); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory(path, directory) { + // strip off leading and trailing '/' so that we can use lastIndexOf when + // determining exactly how a path maps onto a mapped directory -- + // conditional is required here to deal with "/".substring(1, 0) being + // converted to "/".substring(0, 1) per the JS specification + var key = path.length == 1 ? "" : path.substring(1, path.length - 1); + + // the path-to-directory mapping code requires that the first character not + // be "/", or it will go into an infinite loop + if (key.charAt(0) == "/") { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + key = toInternalPath(key, false); + + if (directory) { + dumpn("*** mapping '" + path + "' to the location " + directory.path); + this._pathDirectoryMap.put(key, directory); + } else { + dumpn("*** removing mapping for '" + path + "'"); + this._pathDirectoryMap.put(key, null); + } + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler(err, handler) { + if (!(err in HTTP_ERROR_CODES)) { + dumpn( + "*** WARNING: registering non-HTTP/1.1 error code " + + "(" + + err + + ") handler -- was this intentional?" + ); + } + + this._handlerToField(handler, this._overrideErrors, err); + }, + + // + // see nsIHttpServer.setIndexHandler + // + setIndexHandler(handler) { + if (!handler) { + handler = defaultIndexHandler; + } else if (typeof handler != "function") { + handler = createHandlerFunc(handler); + } + + this._indexHandler = handler; + }, + + // + // see nsIHttpServer.registerContentType + // + registerContentType(ext, type) { + if (!type) { + delete this._mimeMappings[ext]; + } else { + this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type); + } + }, + + // PRIVATE API + + /** + * Sets or remove (if handler is null) a handler in an object with a key. + * + * @param handler + * a handler, either function or an nsIHttpRequestHandler + * @param dict + * The object to attach the handler to. + * @param key + * The field name of the handler. + */ + _handlerToField(handler, dict, key) { + // for convenience, handler can be a function if this is run from xpcshell + if (typeof handler == "function") { + dict[key] = handler; + } else if (handler) { + dict[key] = createHandlerFunc(handler); + } else { + delete dict[key]; + } + }, + + /** + * Handles a request which maps to a file in the local filesystem (if a base + * path has already been set; otherwise the 404 error is thrown). + * + * @param metadata : Request + * metadata for the incoming request + * @param response : Response + * an uninitialized Response to the given request, to be initialized by a + * request handler + * @throws HTTP_### + * if an HTTP error occurred (usually HTTP_404); note that in this case the + * calling code must handle post-processing of the response + */ + _handleDefault(metadata, response) { + dumpn("*** _handleDefault()"); + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + + var path = metadata.path; + NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">"); + + // determine the actual on-disk file; this requires finding the deepest + // path-to-directory mapping in the requested URL + var file = this._getFileForPath(path); + + // the "file" might be a directory, in which case we either serve the + // contained index.html or make the index handler write the response + if (file.exists() && file.isDirectory()) { + file.append("index.html"); // make configurable? + if (!file.exists() || file.isDirectory()) { + metadata._ensurePropertyBag(); + metadata._bag.setPropertyAsInterface("directory", file.parent); + this._indexHandler(metadata, response); + return; + } + } + + // alternately, the file might not exist + if (!file.exists()) { + throw HTTP_404; + } + + var start, end; + if ( + metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) && + metadata.hasHeader("Range") && + this._getTypeFromFile(file) !== SJS_TYPE + ) { + var rangeMatch = metadata + .getHeader("Range") + .match(/^bytes=(\d+)?-(\d+)?$/); + if (!rangeMatch) { + dumpn( + "*** Range header bogosity: '" + metadata.getHeader("Range") + "'" + ); + throw HTTP_400; + } + + if (rangeMatch[1] !== undefined) { + start = parseInt(rangeMatch[1], 10); + } + + if (rangeMatch[2] !== undefined) { + end = parseInt(rangeMatch[2], 10); + } + + if (start === undefined && end === undefined) { + dumpn( + "*** More Range header bogosity: '" + + metadata.getHeader("Range") + + "'" + ); + throw HTTP_400; + } + + // No start given, so the end is really the count of bytes from the + // end of the file. + if (start === undefined) { + start = Math.max(0, file.fileSize - end); + end = file.fileSize - 1; + } + + // start and end are inclusive + if (end === undefined || end >= file.fileSize) { + end = file.fileSize - 1; + } + + if (start !== undefined && start >= file.fileSize) { + var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable"); + HTTP_416.customErrorHandling = function (errorResponse) { + maybeAddHeaders(file, metadata, errorResponse); + }; + throw HTTP_416; + } + + if (end < start) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + start = 0; + end = file.fileSize - 1; + } else { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize; + response.setHeader("Content-Range", contentRange); + } + } else { + start = 0; + end = file.fileSize - 1; + } + + // finally... + dumpn( + "*** handling '" + + path + + "' as mapping to " + + file.path + + " from " + + start + + " to " + + end + + " inclusive" + ); + this._writeFileResponse(metadata, file, response, start, end - start + 1); + }, + + /** + * Writes an HTTP response for the given file, including setting headers for + * file metadata. + * + * @param metadata : Request + * the Request for which a response is being generated + * @param file : nsIFile + * the file which is to be sent in the response + * @param response : Response + * the response to which the file should be written + * @param offset: uint + * the byte offset to skip to when writing + * @param count: uint + * the number of bytes to write + */ + _writeFileResponse(metadata, file, response, offset, count) { + const PR_RDONLY = 0x01; + + var type = this._getTypeFromFile(file); + if (type === SJS_TYPE) { + let fis = new FileInputStream( + file, + PR_RDONLY, + PERMS_READONLY, + Ci.nsIFileInputStream.CLOSE_ON_EOF + ); + + try { + // If you update the list of imports, please update the list in + // tools/lint/eslint/eslint-plugin-mozilla/lib/environments/sjs.js + // as well. + var s = Cu.Sandbox(globalThis); + s.importFunction(dump, "dump"); + s.importFunction(atob, "atob"); + s.importFunction(btoa, "btoa"); + s.importFunction(ChromeUtils, "ChromeUtils"); + s.importFunction(Services, "Services"); + + // Define a basic key-value state-preservation API across requests, with + // keys initially corresponding to the empty string. + var self = this; + var path = metadata.path; + s.importFunction(function getState(k) { + return self._getState(path, k); + }); + s.importFunction(function setState(k, v) { + self._setState(path, k, v); + }); + s.importFunction(function getSharedState(k) { + return self._getSharedState(k); + }); + s.importFunction(function setSharedState(k, v) { + self._setSharedState(k, v); + }); + s.importFunction(function getObjectState(k, callback) { + callback(self._getObjectState(k)); + }); + s.importFunction(function setObjectState(k, v) { + self._setObjectState(k, v); + }); + s.importFunction(function registerPathHandler(p, h) { + self.registerPathHandler(p, h); + }); + + // Make it possible for sjs files to access their location + this._setState(path, "__LOCATION__", file.path); + + try { + // Alas, the line number in errors dumped to console when calling the + // request handler is simply an offset from where we load the SJS file. + // Work around this in a reasonably non-fragile way by dynamically + // getting the line number where we evaluate the SJS file. Don't + // separate these two lines! + var line = new Error().lineNumber; + let uri = Services.io.newFileURI(file); + Services.scriptloader.loadSubScript(uri.spec, s); + } catch (e) { + dumpn("*** syntax error in SJS at " + file.path + ": " + e); + throw HTTP_500; + } + + try { + s.handleRequest(metadata, response); + } catch (e) { + dump( + "*** error running SJS at " + + file.path + + ": " + + e + + " on line " + + (e instanceof Error + ? e.lineNumber + " in httpd.js" + : e.lineNumber - line) + + "\n" + ); + throw HTTP_500; + } + } finally { + fis.close(); + } + } else { + try { + response.setHeader( + "Last-Modified", + toDateString(file.lastModifiedTime), + false + ); + } catch (e) { + /* lastModifiedTime threw, ignore */ + } + + response.setHeader("Content-Type", type, false); + maybeAddInformationalResponse(file, metadata, response); + maybeAddHeaders(file, metadata, response); + // Allow overriding Content-Length + try { + response.getHeader("Content-Length"); + } catch (e) { + response.setHeader("Content-Length", "" + count, false); + } + + let fis = new FileInputStream( + file, + PR_RDONLY, + PERMS_READONLY, + Ci.nsIFileInputStream.CLOSE_ON_EOF + ); + + offset = offset || 0; + count = count || file.fileSize; + NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset"); + NS_ASSERT(count >= 0, "bad count"); + NS_ASSERT(offset + count <= file.fileSize, "bad total data size"); + + try { + if (offset !== 0) { + // Seek (or read, if seeking isn't supported) to the correct offset so + // the data sent to the client matches the requested range. + if (fis instanceof Ci.nsISeekableStream) { + fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset); + } else { + new ScriptableInputStream(fis).read(offset); + } + } + } catch (e) { + fis.close(); + throw e; + } + + let writeMore = function () { + gThreadManager.currentThread.dispatch( + writeData, + Ci.nsIThread.DISPATCH_NORMAL + ); + }; + + var input = new BinaryInputStream(fis); + var output = new BinaryOutputStream(response.bodyOutputStream); + var writeData = { + run() { + var chunkSize = Math.min(65536, count); + count -= chunkSize; + NS_ASSERT(count >= 0, "underflow"); + + try { + var data = input.readByteArray(chunkSize); + NS_ASSERT( + data.length === chunkSize, + "incorrect data returned? got " + + data.length + + ", expected " + + chunkSize + ); + output.writeByteArray(data); + if (count === 0) { + fis.close(); + response.finish(); + } else { + writeMore(); + } + } catch (e) { + try { + fis.close(); + } finally { + response.finish(); + } + throw e; + } + }, + }; + + writeMore(); + + // Now that we know copying will start, flag the response as async. + response.processAsync(); + } + }, + + /** + * Get the value corresponding to a given key for the given path for SJS state + * preservation across requests. + * + * @param path : string + * the path from which the given state is to be retrieved + * @param k : string + * the key whose corresponding value is to be returned + * @returns string + * the corresponding value, which is initially the empty string + */ + _getState(path, k) { + var state = this._state; + if (path in state && k in state[path]) { + return state[path][k]; + } + return ""; + }, + + /** + * Set the value corresponding to a given key for the given path for SJS state + * preservation across requests. + * + * @param path : string + * the path from which the given state is to be retrieved + * @param k : string + * the key whose corresponding value is to be set + * @param v : string + * the value to be set + */ + _setState(path, k, v) { + if (typeof v !== "string") { + throw new Error("non-string value passed"); + } + var state = this._state; + if (!(path in state)) { + state[path] = {}; + } + state[path][k] = v; + }, + + /** + * Get the value corresponding to a given key for SJS state preservation + * across requests. + * + * @param k : string + * the key whose corresponding value is to be returned + * @returns string + * the corresponding value, which is initially the empty string + */ + _getSharedState(k) { + var state = this._sharedState; + if (k in state) { + return state[k]; + } + return ""; + }, + + /** + * Set the value corresponding to a given key for SJS state preservation + * across requests. + * + * @param k : string + * the key whose corresponding value is to be set + * @param v : string + * the value to be set + */ + _setSharedState(k, v) { + if (typeof v !== "string") { + throw new Error("non-string value passed"); + } + this._sharedState[k] = v; + }, + + /** + * Returns the object associated with the given key in the server for SJS + * state preservation across requests. + * + * @param k : string + * the key whose corresponding object is to be returned + * @returns nsISupports + * the corresponding object, or null if none was present + */ + _getObjectState(k) { + if (typeof k !== "string") { + throw new Error("non-string key passed"); + } + return this._objectState[k] || null; + }, + + /** + * Sets the object associated with the given key in the server for SJS + * state preservation across requests. + * + * @param k : string + * the key whose corresponding object is to be set + * @param v : nsISupports + * the object to be associated with the given key; may be null + */ + _setObjectState(k, v) { + if (typeof k !== "string") { + throw new Error("non-string key passed"); + } + if (typeof v !== "object") { + throw new Error("non-object value passed"); + } + if (v && !("QueryInterface" in v)) { + throw new Error( + "must pass an nsISupports; use wrappedJSObject to ease " + + "pain when using the server from JS" + ); + } + + this._objectState[k] = v; + }, + + /** + * Gets a content-type for the given file, first by checking for any custom + * MIME-types registered with this handler for the file's extension, second by + * asking the global MIME service for a content-type, and finally by failing + * over to application/octet-stream. + * + * @param file : nsIFile + * the nsIFile for which to get a file type + * @returns string + * the best content-type which can be determined for the file + */ + _getTypeFromFile(file) { + try { + var name = file.leafName; + var dot = name.lastIndexOf("."); + if (dot > 0) { + var ext = name.slice(dot + 1); + if (ext in this._mimeMappings) { + return this._mimeMappings[ext]; + } + } + return Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromFile(file); + } catch (e) { + return "application/octet-stream"; + } + }, + + /** + * Returns the nsIFile which corresponds to the path, as determined using + * all registered path->directory mappings and any paths which are explicitly + * overridden. + * + * @param path : string + * the server path for which a file should be retrieved, e.g. "/foo/bar" + * @throws HttpError + * when the correct action is the corresponding HTTP error (i.e., because no + * mapping was found for a directory in path, the referenced file doesn't + * exist, etc.) + * @returns nsIFile + * the file to be sent as the response to a request for the path + */ + _getFileForPath(path) { + // decode and add underscores as necessary + try { + path = toInternalPath(path, true); + } catch (e) { + dumpn("*** toInternalPath threw " + e); + throw HTTP_400; // malformed path + } + + // next, get the directory which contains this path + var pathMap = this._pathDirectoryMap; + + // An example progression of tmp for a path "/foo/bar/baz/" might be: + // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", "" + var tmp = path.substring(1); + while (true) { + // do we have a match for current head of the path? + var file = pathMap.get(tmp); + if (file) { + // XXX hack; basically disable showing mapping for /foo/bar/ when the + // requested path was /foo/bar, because relative links on the page + // will all be incorrect -- we really need the ability to easily + // redirect here instead + if ( + tmp == path.substring(1) && + !!tmp.length && + tmp.charAt(tmp.length - 1) != "/" + ) { + file = null; + } else { + break; + } + } + + // if we've finished trying all prefixes, exit + if (tmp == "") { + break; + } + + tmp = tmp.substring(0, tmp.lastIndexOf("/")); + } + + // no mapping applies, so 404 + if (!file) { + throw HTTP_404; + } + + // last, get the file for the path within the determined directory + var parentFolder = file.parent; + var dirIsRoot = parentFolder == null; + + // Strategy here is to append components individually, making sure we + // never move above the given directory; this allows paths such as + // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling"; + // this component-wise approach also means the code works even on platforms + // which don't use "/" as the directory separator, such as Windows + var leafPath = path.substring(tmp.length + 1); + var comps = leafPath.split("/"); + for (var i = 0, sz = comps.length; i < sz; i++) { + var comp = comps[i]; + + if (comp == "..") { + file = file.parent; + } else if (comp == "." || comp == "") { + continue; + } else { + file.append(comp); + } + + if (!dirIsRoot && file.equals(parentFolder)) { + throw HTTP_403; + } + } + + return file; + }, + + /** + * Writes the error page for the given HTTP error code over the given + * connection. + * + * @param errorCode : uint + * the HTTP error code to be used + * @param connection : Connection + * the connection on which the error occurred + */ + handleError(errorCode, connection) { + var response = new Response(connection); + + dumpn("*** error in request: " + errorCode); + + this._handleError(errorCode, new Request(connection.port), response); + }, + + /** + * Handles a request which generates the given error code, using the + * user-defined error handler if one has been set, gracefully falling back to + * the x00 status code if the code has no handler, and failing to status code + * 500 if all else fails. + * + * @param errorCode : uint + * the HTTP error which is to be returned + * @param metadata : Request + * metadata for the request, which will often be incomplete since this is an + * error + * @param response : Response + * an uninitialized Response should be initialized when this method + * completes with information which represents the desired error code in the + * ideal case or a fallback code in abnormal circumstances (i.e., 500 is a + * fallback for 505, per HTTP specs) + */ + _handleError(errorCode, metadata, response) { + if (!metadata) { + throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER); + } + + var errorX00 = errorCode - (errorCode % 100); + + try { + if (!(errorCode in HTTP_ERROR_CODES)) { + dumpn("*** WARNING: requested invalid error: " + errorCode); + } + + // RFC 2616 says that we should try to handle an error by its class if we + // can't otherwise handle it -- if that fails, we revert to handling it as + // a 500 internal server error, and if that fails we throw and shut down + // the server + + // actually handle the error + try { + if (errorCode in this._overrideErrors) { + this._overrideErrors[errorCode](metadata, response); + } else { + this._defaultErrors[errorCode](metadata, response); + } + } catch (e) { + if (response.partiallySent()) { + response.abort(e); + return; + } + + // don't retry the handler that threw + if (errorX00 == errorCode) { + throw HTTP_500; + } + + dumpn( + "*** error in handling for error code " + + errorCode + + ", " + + "falling back to " + + errorX00 + + "..." + ); + response = new Response(response._connection); + if (errorX00 in this._overrideErrors) { + this._overrideErrors[errorX00](metadata, response); + } else if (errorX00 in this._defaultErrors) { + this._defaultErrors[errorX00](metadata, response); + } else { + throw HTTP_500; + } + } + } catch (e) { + if (response.partiallySent()) { + response.abort(); + return; + } + + // we've tried everything possible for a meaningful error -- now try 500 + dumpn( + "*** error in handling for error code " + + errorX00 + + ", falling " + + "back to 500..." + ); + + try { + response = new Response(response._connection); + if (500 in this._overrideErrors) { + this._overrideErrors[500](metadata, response); + } else { + this._defaultErrors[500](metadata, response); + } + } catch (e2) { + dumpn("*** multiple errors in default error handlers!"); + dumpn("*** e == " + e + ", e2 == " + e2); + response.abort(e2); + return; + } + } + + response.complete(); + }, + + // FIELDS + + /** + * This object contains the default handlers for the various HTTP error codes. + */ + _defaultErrors: { + 400(metadata, response) { + // none of the data in metadata is reliable, so hard-code everything here + response.setStatusLine("1.1", 400, "Bad Request"); + response.setHeader("Content-Type", "text/plain;charset=utf-8", false); + + var body = "Bad request\n"; + response.bodyOutputStream.write(body, body.length); + }, + 403(metadata, response) { + response.setStatusLine(metadata.httpVersion, 403, "Forbidden"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = + "<html>\ + <head><title>403 Forbidden</title></head>\ + <body>\ + <h1>403 Forbidden</h1>\ + </body>\ + </html>"; + response.bodyOutputStream.write(body, body.length); + }, + 404(metadata, response) { + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = + "<html>\ + <head><title>404 Not Found</title></head>\ + <body>\ + <h1>404 Not Found</h1>\ + <p>\ + <span style='font-family: monospace;'>" + + htmlEscape(metadata.path) + + "</span> was not found.\ + </p>\ + </body>\ + </html>"; + response.bodyOutputStream.write(body, body.length); + }, + 416(metadata, response) { + response.setStatusLine( + metadata.httpVersion, + 416, + "Requested Range Not Satisfiable" + ); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = + "<html>\ + <head>\ + <title>416 Requested Range Not Satisfiable</title></head>\ + <body>\ + <h1>416 Requested Range Not Satisfiable</h1>\ + <p>The byte range was not valid for the\ + requested resource.\ + </p>\ + </body>\ + </html>"; + response.bodyOutputStream.write(body, body.length); + }, + 500(metadata, response) { + response.setStatusLine( + metadata.httpVersion, + 500, + "Internal Server Error" + ); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = + "<html>\ + <head><title>500 Internal Server Error</title></head>\ + <body>\ + <h1>500 Internal Server Error</h1>\ + <p>Something's broken in this server and\ + needs to be fixed.</p>\ + </body>\ + </html>"; + response.bodyOutputStream.write(body, body.length); + }, + 501(metadata, response) { + response.setStatusLine(metadata.httpVersion, 501, "Not Implemented"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = + "<html>\ + <head><title>501 Not Implemented</title></head>\ + <body>\ + <h1>501 Not Implemented</h1>\ + <p>This server is not (yet) Apache.</p>\ + </body>\ + </html>"; + response.bodyOutputStream.write(body, body.length); + }, + 505(metadata, response) { + response.setStatusLine("1.1", 505, "HTTP Version Not Supported"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = + "<html>\ + <head><title>505 HTTP Version Not Supported</title></head>\ + <body>\ + <h1>505 HTTP Version Not Supported</h1>\ + <p>This server only supports HTTP/1.0 and HTTP/1.1\ + connections.</p>\ + </body>\ + </html>"; + response.bodyOutputStream.write(body, body.length); + }, + }, + + /** + * Contains handlers for the default set of URIs contained in this server. + */ + _defaultPaths: { + "/": function (metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + + var body = + "<html>\ + <head><title>httpd.js</title></head>\ + <body>\ + <h1>httpd.js</h1>\ + <p>If you're seeing this page, httpd.js is up and\ + serving requests! Now set a base path and serve some\ + files!</p>\ + </body>\ + </html>"; + + response.bodyOutputStream.write(body, body.length); + }, + + "/trace": function (metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain;charset=utf-8", false); + + var body = + "Request-URI: " + + metadata.scheme + + "://" + + metadata.host + + ":" + + metadata.port + + metadata.path + + "\n\n"; + body += "Request (semantically equivalent, slightly reformatted):\n\n"; + body += metadata.method + " " + metadata.path; + + if (metadata.queryString) { + body += "?" + metadata.queryString; + } + + body += " HTTP/" + metadata.httpVersion + "\r\n"; + + var headEnum = metadata.headers; + while (headEnum.hasMoreElements()) { + var fieldName = headEnum + .getNext() + .QueryInterface(Ci.nsISupportsString).data; + body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n"; + } + + response.bodyOutputStream.write(body, body.length); + }, + }, +}; + +/** + * Maps absolute paths to files on the local file system (as nsILocalFiles). + */ +function FileMap() { + /** Hash which will map paths to nsILocalFiles. */ + this._map = {}; +} +FileMap.prototype = { + // PUBLIC API + + /** + * Maps key to a clone of the nsIFile value if value is non-null; + * otherwise, removes any extant mapping for key. + * + * @param key : string + * string to which a clone of value is mapped + * @param value : nsIFile + * the file to map to key, or null to remove a mapping + */ + put(key, value) { + if (value) { + this._map[key] = value.clone(); + } else { + delete this._map[key]; + } + }, + + /** + * Returns a clone of the nsIFile mapped to key, or null if no such + * mapping exists. + * + * @param key : string + * key to which the returned file maps + * @returns nsIFile + * a clone of the mapped file, or null if no mapping exists + */ + get(key) { + var val = this._map[key]; + return val ? val.clone() : null; + }, +}; + +// Response CONSTANTS + +// token = *<any CHAR except CTLs or separators> +// CHAR = <any US-ASCII character (0-127)> +// CTL = <any US-ASCII control character (0-31) and DEL (127)> +// separators = "(" | ")" | "<" | ">" | "@" +// | "," | ";" | ":" | "\" | <"> +// | "/" | "[" | "]" | "?" | "=" +// | "{" | "}" | SP | HT +const IS_TOKEN_ARRAY = [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 0 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 8 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 16 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 24 + + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, // 32 + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 0, // 40 + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, // 48 + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, // 56 + + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, // 64 + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, // 72 + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, // 80 + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, // 88 + + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, // 96 + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, // 104 + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, // 112 + 1, + 1, + 1, + 0, + 1, + 0, + 1, +]; // 120 + +/** + * Determines whether the given character code is a CTL. + * + * @param code : uint + * the character code + * @returns boolean + * true if code is a CTL, false otherwise + */ +function isCTL(code) { + return (code >= 0 && code <= 31) || code == 127; +} + +/** + * Represents a response to an HTTP request, encapsulating all details of that + * response. This includes all headers, the HTTP version, status code and + * explanation, and the entity itself. + * + * @param connection : Connection + * the connection over which this response is to be written + */ +function Response(connection) { + /** The connection over which this response will be written. */ + this._connection = connection; + + /** + * The HTTP version of this response; defaults to 1.1 if not set by the + * handler. + */ + this._httpVersion = nsHttpVersion.HTTP_1_1; + + /** + * The HTTP code of this response; defaults to 200. + */ + this._httpCode = 200; + + /** + * The description of the HTTP code in this response; defaults to "OK". + */ + this._httpDescription = "OK"; + + /** + * An nsIHttpHeaders object in which the headers in this response should be + * stored. This property is null after the status line and headers have been + * written to the network, and it may be modified up until it is cleared, + * except if this._finished is set first (in which case headers are written + * asynchronously in response to a finish() call not preceded by + * flushHeaders()). + */ + this._headers = new nsHttpHeaders(); + + /** + * Informational response: + * For example 103 Early Hint + **/ + this._informationalResponseHttpVersion = nsHttpVersion.HTTP_1_1; + this._informationalResponseHttpCode = 0; + this._informationalResponseHttpDescription = ""; + this._informationalResponseHeaders = new nsHttpHeaders(); + this._informationalResponseSet = false; + + /** + * Set to true when this response is ended (completely constructed if possible + * and the connection closed); further actions on this will then fail. + */ + this._ended = false; + + /** + * A stream used to hold data written to the body of this response. + */ + this._bodyOutputStream = null; + + /** + * A stream containing all data that has been written to the body of this + * response so far. (Async handlers make the data contained in this + * unreliable as a way of determining content length in general, but auxiliary + * saved information can sometimes be used to guarantee reliability.) + */ + this._bodyInputStream = null; + + /** + * A stream copier which copies data to the network. It is initially null + * until replaced with a copier for response headers; when headers have been + * fully sent it is replaced with a copier for the response body, remaining + * so for the duration of response processing. + */ + this._asyncCopier = null; + + /** + * True if this response has been designated as being processed + * asynchronously rather than for the duration of a single call to + * nsIHttpRequestHandler.handle. + */ + this._processAsync = false; + + /** + * True iff finish() has been called on this, signaling that no more changes + * to this may be made. + */ + this._finished = false; + + /** + * True iff powerSeized() has been called on this, signaling that this + * response is to be handled manually by the response handler (which may then + * send arbitrary data in response, even non-HTTP responses). + */ + this._powerSeized = false; +} +Response.prototype = { + // PUBLIC CONSTRUCTION API + + // + // see nsIHttpResponse.bodyOutputStream + // + get bodyOutputStream() { + if (this._finished) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + + if (!this._bodyOutputStream) { + var pipe = new Pipe( + true, + false, + Response.SEGMENT_SIZE, + PR_UINT32_MAX, + null + ); + this._bodyOutputStream = pipe.outputStream; + this._bodyInputStream = pipe.inputStream; + if (this._processAsync || this._powerSeized) { + this._startAsyncProcessor(); + } + } + + return this._bodyOutputStream; + }, + + // + // see nsIHttpResponse.write + // + write(data) { + if (this._finished) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + + var dataAsString = String(data); + this.bodyOutputStream.write(dataAsString, dataAsString.length); + }, + + // + // see nsIHttpResponse.setStatusLine + // + setStatusLineInternal(httpVersion, code, description, informationalResponse) { + if (this._finished || this._powerSeized) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + + if (!informationalResponse) { + if (!this._headers) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + } else if (!this._informationalResponseHeaders) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + this._ensureAlive(); + + if (!(code >= 0 && code < 1000)) { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + try { + var httpVer; + // avoid version construction for the most common cases + if (!httpVersion || httpVersion == "1.1") { + httpVer = nsHttpVersion.HTTP_1_1; + } else if (httpVersion == "1.0") { + httpVer = nsHttpVersion.HTTP_1_0; + } else { + httpVer = new nsHttpVersion(httpVersion); + } + } catch (e) { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + // Reason-Phrase = *<TEXT, excluding CR, LF> + // TEXT = <any OCTET except CTLs, but including LWS> + // + // XXX this ends up disallowing octets which aren't Unicode, I think -- not + // much to do if description is IDL'd as string + if (!description) { + description = ""; + } + for (var i = 0; i < description.length; i++) { + if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t") { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + } + + // set the values only after validation to preserve atomicity + if (!informationalResponse) { + this._httpDescription = description; + this._httpCode = code; + this._httpVersion = httpVer; + } else { + this._informationalResponseSet = true; + this._informationalResponseHttpDescription = description; + this._informationalResponseHttpCode = code; + this._informationalResponseHttpVersion = httpVer; + } + }, + + // + // see nsIHttpResponse.setStatusLine + // + setStatusLine(httpVersion, code, description) { + this.setStatusLineInternal(httpVersion, code, description, false); + }, + + setInformationalResponseStatusLine(httpVersion, code, description) { + this.setStatusLineInternal(httpVersion, code, description, true); + }, + + // + // see nsIHttpResponse.setHeader + // + setHeader(name, value, merge) { + if (!this._headers || this._finished || this._powerSeized) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + this._ensureAlive(); + + this._headers.setHeader(name, value, merge); + }, + + setInformationalResponseHeader(name, value, merge) { + if ( + !this._informationalResponseHeaders || + this._finished || + this._powerSeized + ) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + this._ensureAlive(); + + this._informationalResponseHeaders.setHeader(name, value, merge); + }, + + setHeaderNoCheck(name, value) { + if (!this._headers || this._finished || this._powerSeized) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + this._ensureAlive(); + + this._headers.setHeaderNoCheck(name, value); + }, + + setInformationalHeaderNoCheck(name, value) { + if (!this._headers || this._finished || this._powerSeized) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + this._ensureAlive(); + + this._informationalResponseHeaders.setHeaderNoCheck(name, value); + }, + + // + // see nsIHttpResponse.processAsync + // + processAsync() { + if (this._finished) { + throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); + } + if (this._powerSeized) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + if (this._processAsync) { + return; + } + this._ensureAlive(); + + dumpn("*** processing connection " + this._connection.number + " async"); + this._processAsync = true; + + /* + * Either the bodyOutputStream getter or this method is responsible for + * starting the asynchronous processor and catching writes of data to the + * response body of async responses as they happen, for the purpose of + * forwarding those writes to the actual connection's output stream. + * If bodyOutputStream is accessed first, calling this method will create + * the processor (when it first is clear that body data is to be written + * immediately, not buffered). If this method is called first, accessing + * bodyOutputStream will create the processor. If only this method is + * called, we'll write nothing, neither headers nor the nonexistent body, + * until finish() is called. Since that delay is easily avoided by simply + * getting bodyOutputStream or calling write(""), we don't worry about it. + */ + if (this._bodyOutputStream && !this._asyncCopier) { + this._startAsyncProcessor(); + } + }, + + // + // see nsIHttpResponse.seizePower + // + seizePower() { + if (this._processAsync) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + if (this._finished) { + throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); + } + if (this._powerSeized) { + return; + } + this._ensureAlive(); + + dumpn( + "*** forcefully seizing power over connection " + + this._connection.number + + "..." + ); + + // Purge any already-written data without sending it. We could as easily + // swap out the streams entirely, but that makes it possible to acquire and + // unknowingly use a stale reference, so we require there only be one of + // each stream ever for any response to avoid this complication. + if (this._asyncCopier) { + this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED); + } + this._asyncCopier = null; + if (this._bodyOutputStream) { + var input = new BinaryInputStream(this._bodyInputStream); + var avail; + while ((avail = input.available()) > 0) { + input.readByteArray(avail); + } + } + + this._powerSeized = true; + if (this._bodyOutputStream) { + this._startAsyncProcessor(); + } + }, + + // + // see nsIHttpResponse.finish + // + finish() { + if (!this._processAsync && !this._powerSeized) { + throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); + } + if (this._finished) { + return; + } + + dumpn("*** finishing connection " + this._connection.number); + this._startAsyncProcessor(); // in case bodyOutputStream was never accessed + if (this._bodyOutputStream) { + this._bodyOutputStream.close(); + } + this._finished = true; + }, + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: ChromeUtils.generateQI(["nsIHttpResponse"]), + + // POST-CONSTRUCTION API (not exposed externally) + + /** + * The HTTP version number of this, as a string (e.g. "1.1"). + */ + get httpVersion() { + this._ensureAlive(); + return this._httpVersion.toString(); + }, + + /** + * The HTTP status code of this response, as a string of three characters per + * RFC 2616. + */ + get httpCode() { + this._ensureAlive(); + + var codeString = + (this._httpCode < 10 ? "0" : "") + + (this._httpCode < 100 ? "0" : "") + + this._httpCode; + return codeString; + }, + + /** + * The description of the HTTP status code of this response, or "" if none is + * set. + */ + get httpDescription() { + this._ensureAlive(); + + return this._httpDescription; + }, + + /** + * The headers in this response, as an nsHttpHeaders object. + */ + get headers() { + this._ensureAlive(); + + return this._headers; + }, + + // + // see nsHttpHeaders.getHeader + // + getHeader(name) { + this._ensureAlive(); + + return this._headers.getHeader(name); + }, + + /** + * Determines whether this response may be abandoned in favor of a newly + * constructed response. A response may be abandoned only if it is not being + * sent asynchronously and if raw control over it has not been taken from the + * server. + * + * @returns boolean + * true iff no data has been written to the network + */ + partiallySent() { + dumpn("*** partiallySent()"); + return this._processAsync || this._powerSeized; + }, + + /** + * If necessary, kicks off the remaining request processing needed to be done + * after a request handler performs its initial work upon this response. + */ + complete() { + dumpn("*** complete()"); + if (this._processAsync || this._powerSeized) { + NS_ASSERT( + this._processAsync ^ this._powerSeized, + "can't both send async and relinquish power" + ); + return; + } + + NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?"); + + this._startAsyncProcessor(); + + // Now make sure we finish processing this request! + if (this._bodyOutputStream) { + this._bodyOutputStream.close(); + } + }, + + /** + * Abruptly ends processing of this response, usually due to an error in an + * incoming request but potentially due to a bad error handler. Since we + * cannot handle the error in the usual way (giving an HTTP error page in + * response) because data may already have been sent (or because the response + * might be expected to have been generated asynchronously or completely from + * scratch by the handler), we stop processing this response and abruptly + * close the connection. + * + * @param e : Error + * the exception which precipitated this abort, or null if no such exception + * was generated + * @param truncateConnection : Boolean + * ensures that we truncate the connection using an RST packet, so the + * client testing code is aware that an error occurred, otherwise it may + * consider the response as valid. + */ + abort(e, truncateConnection = false) { + dumpn("*** abort(<" + e + ">)"); + + if (truncateConnection) { + dumpn("*** truncate connection"); + this._connection.transport.setLinger(true, 0); + } + + // This response will be ended by the processor if one was created. + var copier = this._asyncCopier; + if (copier) { + // We dispatch asynchronously here so that any pending writes of data to + // the connection will be deterministically written. This makes it easier + // to specify exact behavior, and it makes observable behavior more + // predictable for clients. Note that the correctness of this depends on + // callbacks in response to _waitToReadData in WriteThroughCopier + // happening asynchronously with respect to the actual writing of data to + // bodyOutputStream, as they currently do; if they happened synchronously, + // an event which ran before this one could write more data to the + // response body before we get around to canceling the copier. We have + // tests for this in test_seizepower.js, however, and I can't think of a + // way to handle both cases without removing bodyOutputStream access and + // moving its effective write(data, length) method onto Response, which + // would be slower and require more code than this anyway. + gThreadManager.currentThread.dispatch( + { + run() { + dumpn("*** canceling copy asynchronously..."); + copier.cancel(Cr.NS_ERROR_UNEXPECTED); + }, + }, + Ci.nsIThread.DISPATCH_NORMAL + ); + } else { + this.end(); + } + }, + + /** + * Closes this response's network connection, marks the response as finished, + * and notifies the server handler that the request is done being processed. + */ + end() { + NS_ASSERT(!this._ended, "ending this response twice?!?!"); + + this._connection.close(); + if (this._bodyOutputStream) { + this._bodyOutputStream.close(); + } + + this._finished = true; + this._ended = true; + }, + + // PRIVATE IMPLEMENTATION + + /** + * Sends the status line and headers of this response if they haven't been + * sent and initiates the process of copying data written to this response's + * body to the network. + */ + _startAsyncProcessor() { + dumpn("*** _startAsyncProcessor()"); + + // Handle cases where we're being called a second time. The former case + // happens when this is triggered both by complete() and by processAsync(), + // while the latter happens when processAsync() in conjunction with sent + // data causes abort() to be called. + if (this._asyncCopier || this._ended) { + dumpn("*** ignoring second call to _startAsyncProcessor"); + return; + } + + // Send headers if they haven't been sent already and should be sent, then + // asynchronously continue to send the body. + if (this._headers && !this._powerSeized) { + this._sendHeaders(); + return; + } + + this._headers = null; + this._sendBody(); + }, + + /** + * Signals that all modifications to the response status line and headers are + * complete and then sends that data over the network to the client. Once + * this method completes, a different response to the request that resulted + * in this response cannot be sent -- the only possible action in case of + * error is to abort the response and close the connection. + */ + _sendHeaders() { + dumpn("*** _sendHeaders()"); + + NS_ASSERT(this._headers); + NS_ASSERT(this._informationalResponseHeaders); + NS_ASSERT(!this._powerSeized); + + var preambleData = []; + + // Informational response, e.g. 103 + if (this._informationalResponseSet) { + // request-line + let statusLine = + "HTTP/" + + this._informationalResponseHttpVersion + + " " + + this._informationalResponseHttpCode + + " " + + this._informationalResponseHttpDescription + + "\r\n"; + preambleData.push(statusLine); + + // headers + let headEnum = this._informationalResponseHeaders.enumerator; + while (headEnum.hasMoreElements()) { + let fieldName = headEnum + .getNext() + .QueryInterface(Ci.nsISupportsString).data; + let values = + this._informationalResponseHeaders.getHeaderValues(fieldName); + for (let i = 0, sz = values.length; i < sz; i++) { + preambleData.push(fieldName + ": " + values[i] + "\r\n"); + } + } + // end request-line/headers + preambleData.push("\r\n"); + } + + // request-line + var statusLine = + "HTTP/" + + this.httpVersion + + " " + + this.httpCode + + " " + + this.httpDescription + + "\r\n"; + + // header post-processing + + var headers = this._headers; + headers.setHeader("Connection", "close", false); + headers.setHeader("Server", "httpd.js", false); + if (!headers.hasHeader("Date")) { + headers.setHeader("Date", toDateString(Date.now()), false); + } + + // Any response not being processed asynchronously must have an associated + // Content-Length header for reasons of backwards compatibility with the + // initial server, which fully buffered every response before sending it. + // Beyond that, however, it's good to do this anyway because otherwise it's + // impossible to test behaviors that depend on the presence or absence of a + // Content-Length header. + if (!this._processAsync) { + dumpn("*** non-async response, set Content-Length"); + + var bodyStream = this._bodyInputStream; + var avail = bodyStream ? bodyStream.available() : 0; + + // XXX assumes stream will always report the full amount of data available + headers.setHeader("Content-Length", "" + avail, false); + } + + // construct and send response + dumpn("*** header post-processing completed, sending response head..."); + + // request-line + preambleData.push(statusLine); + + // headers + var headEnum = headers.enumerator; + while (headEnum.hasMoreElements()) { + var fieldName = headEnum + .getNext() + .QueryInterface(Ci.nsISupportsString).data; + var values = headers.getHeaderValues(fieldName); + for (var i = 0, sz = values.length; i < sz; i++) { + preambleData.push(fieldName + ": " + values[i] + "\r\n"); + } + } + + // end request-line/headers + preambleData.push("\r\n"); + + var preamble = preambleData.join(""); + + var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null); + responseHeadPipe.outputStream.write(preamble, preamble.length); + + var response = this; + var copyObserver = { + onStartRequest(request) { + dumpn("*** preamble copying started"); + }, + + onStopRequest(request, statusCode) { + dumpn( + "*** preamble copying complete " + + "[status=0x" + + statusCode.toString(16) + + "]" + ); + + if (!Components.isSuccessCode(statusCode)) { + dumpn( + "!!! header copying problems: non-success statusCode, " + + "ending response" + ); + + response.end(); + } else { + response._sendBody(); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]), + }; + + this._asyncCopier = new WriteThroughCopier( + responseHeadPipe.inputStream, + this._connection.output, + copyObserver, + null + ); + + responseHeadPipe.outputStream.close(); + + // Forbid setting any more headers or modifying the request line. + this._headers = null; + }, + + /** + * Asynchronously writes the body of the response (or the entire response, if + * seizePower() has been called) to the network. + */ + _sendBody() { + dumpn("*** _sendBody"); + + NS_ASSERT(!this._headers, "still have headers around but sending body?"); + + // If no body data was written, we're done + if (!this._bodyInputStream) { + dumpn("*** empty body, response finished"); + this.end(); + return; + } + + var response = this; + var copyObserver = { + onStartRequest(request) { + dumpn("*** onStartRequest"); + }, + + onStopRequest(request, statusCode) { + dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]"); + + if (statusCode === Cr.NS_BINDING_ABORTED) { + dumpn("*** terminating copy observer without ending the response"); + } else { + if (!Components.isSuccessCode(statusCode)) { + dumpn("*** WARNING: non-success statusCode in onStopRequest"); + } + + response.end(); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]), + }; + + dumpn("*** starting async copier of body data..."); + this._asyncCopier = new WriteThroughCopier( + this._bodyInputStream, + this._connection.output, + copyObserver, + null + ); + }, + + /** Ensures that this hasn't been ended. */ + _ensureAlive() { + NS_ASSERT(!this._ended, "not handling response lifetime correctly"); + }, +}; + +/** + * Size of the segments in the buffer used in storing response data and writing + * it to the socket. + */ +Response.SEGMENT_SIZE = 8192; + +/** Serves double duty in WriteThroughCopier implementation. */ +function notImplemented() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); +} + +/** Returns true iff the given exception represents stream closure. */ +function streamClosed(e) { + return ( + e === Cr.NS_BASE_STREAM_CLOSED || + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED) + ); +} + +/** Returns true iff the given exception represents a blocked stream. */ +function wouldBlock(e) { + return ( + e === Cr.NS_BASE_STREAM_WOULD_BLOCK || + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK) + ); +} + +/** + * Copies data from source to sink as it becomes available, when that data can + * be written to sink without blocking. + * + * @param source : nsIAsyncInputStream + * the stream from which data is to be read + * @param sink : nsIAsyncOutputStream + * the stream to which data is to be copied + * @param observer : nsIRequestObserver + * an observer which will be notified when the copy starts and finishes + * @param context : nsISupports + * context passed to observer when notified of start/stop + * @throws NS_ERROR_NULL_POINTER + * if source, sink, or observer are null + */ +function WriteThroughCopier(source, sink, observer, context) { + if (!source || !sink || !observer) { + throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER); + } + + /** Stream from which data is being read. */ + this._source = source; + + /** Stream to which data is being written. */ + this._sink = sink; + + /** Observer watching this copy. */ + this._observer = observer; + + /** Context for the observer watching this. */ + this._context = context; + + /** + * True iff this is currently being canceled (cancel has been called, the + * callback may not yet have been made). + */ + this._canceled = false; + + /** + * False until all data has been read from input and written to output, at + * which point this copy is completed and cancel() is asynchronously called. + */ + this._completed = false; + + /** Required by nsIRequest, meaningless. */ + this.loadFlags = 0; + /** Required by nsIRequest, meaningless. */ + this.loadGroup = null; + /** Required by nsIRequest, meaningless. */ + this.name = "response-body-copy"; + + /** Status of this request. */ + this.status = Cr.NS_OK; + + /** Arrays of byte strings waiting to be written to output. */ + this._pendingData = []; + + // start copying + try { + observer.onStartRequest(this); + this._waitToReadData(); + this._waitForSinkClosure(); + } catch (e) { + dumpn( + "!!! error starting copy: " + + e + + ("lineNumber" in e ? ", line " + e.lineNumber : "") + ); + dumpn(e.stack); + this.cancel(Cr.NS_ERROR_UNEXPECTED); + } +} +WriteThroughCopier.prototype = { + /* nsISupports implementation */ + + QueryInterface: ChromeUtils.generateQI([ + "nsIInputStreamCallback", + "nsIOutputStreamCallback", + "nsIRequest", + ]), + + // NSIINPUTSTREAMCALLBACK + + /** + * Receives a more-data-in-input notification and writes the corresponding + * data to the output. + * + * @param input : nsIAsyncInputStream + * the input stream on whose data we have been waiting + */ + onInputStreamReady(input) { + if (this._source === null) { + return; + } + + dumpn("*** onInputStreamReady"); + + // + // Ordinarily we'll read a non-zero amount of data from input, queue it up + // to be written and then wait for further callbacks. The complications in + // this method are the cases where we deviate from that behavior when errors + // occur or when copying is drawing to a finish. + // + // The edge cases when reading data are: + // + // Zero data is read + // If zero data was read, we're at the end of available data, so we can + // should stop reading and move on to writing out what we have (or, if + // we've already done that, onto notifying of completion). + // A stream-closed exception is thrown + // This is effectively a less kind version of zero data being read; the + // only difference is that we notify of completion with that result + // rather than with NS_OK. + // Some other exception is thrown + // This is the least kind result. We don't know what happened, so we + // act as though the stream closed except that we notify of completion + // with the result NS_ERROR_UNEXPECTED. + // + + var bytesWanted = 0, + bytesConsumed = -1; + try { + input = new BinaryInputStream(input); + + bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE); + dumpn("*** input wanted: " + bytesWanted); + + if (bytesWanted > 0) { + var data = input.readByteArray(bytesWanted); + bytesConsumed = data.length; + this._pendingData.push(String.fromCharCode.apply(String, data)); + } + + dumpn("*** " + bytesConsumed + " bytes read"); + + // Handle the zero-data edge case in the same place as all other edge + // cases are handled. + if (bytesWanted === 0) { + throw Components.Exception("", Cr.NS_BASE_STREAM_CLOSED); + } + } catch (e) { + let rv; + if (streamClosed(e)) { + dumpn("*** input stream closed"); + rv = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED; + } else { + dumpn("!!! unexpected error reading from input, canceling: " + e); + rv = Cr.NS_ERROR_UNEXPECTED; + } + + this._doneReadingSource(rv); + return; + } + + var pendingData = this._pendingData; + + NS_ASSERT(bytesConsumed > 0); + NS_ASSERT(!!pendingData.length, "no pending data somehow?"); + NS_ASSERT( + !!pendingData[pendingData.length - 1].length, + "buffered zero bytes of data?" + ); + + NS_ASSERT(this._source !== null); + + // Reading has gone great, and we've gotten data to write now. What if we + // don't have a place to write that data, because output went away just + // before this read? Drop everything on the floor, including new data, and + // cancel at this point. + if (this._sink === null) { + pendingData.length = 0; + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Okay, we've read the data, and we know we have a place to write it. We + // need to queue up the data to be written, but *only* if none is queued + // already -- if data's already queued, the code that actually writes the + // data will make sure to wait on unconsumed pending data. + try { + if (pendingData.length === 1) { + this._waitToWriteData(); + } + } catch (e) { + dumpn( + "!!! error waiting to write data just read, swallowing and " + + "writing only what we already have: " + + e + ); + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Whee! We successfully read some data, and it's successfully queued up to + // be written. All that remains now is to wait for more data to read. + try { + this._waitToReadData(); + } catch (e) { + dumpn("!!! error waiting to read more data: " + e); + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); + } + }, + + // NSIOUTPUTSTREAMCALLBACK + + /** + * Callback when data may be written to the output stream without blocking, or + * when the output stream has been closed. + * + * @param output : nsIAsyncOutputStream + * the output stream on whose writability we've been waiting, also known as + * this._sink + */ + onOutputStreamReady(output) { + if (this._sink === null) { + return; + } + + dumpn("*** onOutputStreamReady"); + + var pendingData = this._pendingData; + if (pendingData.length === 0) { + // There's no pending data to write. The only way this can happen is if + // we're waiting on the output stream's closure, so we can respond to a + // copying failure as quickly as possible (rather than waiting for data to + // be available to read and then fail to be copied). Therefore, we must + // be done now -- don't bother to attempt to write anything and wrap + // things up. + dumpn("!!! output stream closed prematurely, ending copy"); + + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + NS_ASSERT(!!pendingData[0].length, "queued up an empty quantum?"); + + // + // Write out the first pending quantum of data. The possible errors here + // are: + // + // The write might fail because we can't write that much data + // Okay, we've written what we can now, so re-queue what's left and + // finish writing it out later. + // The write failed because the stream was closed + // Discard pending data that we can no longer write, stop reading, and + // signal that copying finished. + // Some other error occurred. + // Same as if the stream were closed, but notify with the status + // NS_ERROR_UNEXPECTED so the observer knows something was wonky. + // + + try { + var quantum = pendingData[0]; + + // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on + // undefined behavior! We're only using this because writeByteArray + // is unusably broken for asynchronous output streams; see bug 532834 + // for details. + var bytesWritten = output.write(quantum, quantum.length); + if (bytesWritten === quantum.length) { + pendingData.shift(); + } else { + pendingData[0] = quantum.substring(bytesWritten); + } + + dumpn("*** wrote " + bytesWritten + " bytes of data"); + } catch (e) { + if (wouldBlock(e)) { + NS_ASSERT( + !!pendingData.length, + "stream-blocking exception with no data to write?" + ); + NS_ASSERT( + !!pendingData[0].length, + "stream-blocking exception with empty quantum?" + ); + this._waitToWriteData(); + return; + } + + if (streamClosed(e)) { + dumpn("!!! output stream prematurely closed, signaling error..."); + } else { + dumpn("!!! unknown error: " + e + ", quantum=" + quantum); + } + + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // The day is ours! Quantum written, now let's see if we have more data + // still to write. + try { + if (pendingData.length) { + this._waitToWriteData(); + return; + } + } catch (e) { + dumpn("!!! unexpected error waiting to write pending data: " + e); + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Okay, we have no more pending data to write -- but might we get more in + // the future? + if (this._source !== null) { + /* + * If we might, then wait for the output stream to be closed. (We wait + * only for closure because we have no data to write -- and if we waited + * for a specific amount of data, we would get repeatedly notified for no + * reason if over time the output stream permitted more and more data to + * be written to it without blocking.) + */ + this._waitForSinkClosure(); + } else { + /* + * On the other hand, if we can't have more data because the input + * stream's gone away, then it's time to notify of copy completion. + * Victory! + */ + this._sink = null; + this._cancelOrDispatchCancelCallback(Cr.NS_OK); + } + }, + + // NSIREQUEST + + /** Returns true if the cancel observer hasn't been notified yet. */ + isPending() { + return !this._completed; + }, + + /** Not implemented, don't use! */ + suspend: notImplemented, + /** Not implemented, don't use! */ + resume: notImplemented, + + /** + * Cancels data reading from input, asynchronously writes out any pending + * data, and causes the observer to be notified with the given error code when + * all writing has finished. + * + * @param status : nsresult + * the status to pass to the observer when data copying has been canceled + */ + cancel(status) { + dumpn("*** cancel(" + status.toString(16) + ")"); + + if (this._canceled) { + dumpn("*** suppressing a late cancel"); + return; + } + + this._canceled = true; + this.status = status; + + // We could be in the middle of absolutely anything at this point. Both + // input and output might still be around, we might have pending data to + // write, and in general we know nothing about the state of the world. We + // therefore must assume everything's in progress and take everything to its + // final steady state (or so far as it can go before we need to finish + // writing out remaining data). + + this._doneReadingSource(status); + }, + + // PRIVATE IMPLEMENTATION + + /** + * Stop reading input if we haven't already done so, passing e as the status + * when closing the stream, and kick off a copy-completion notice if no more + * data remains to be written. + * + * @param e : nsresult + * the status to be used when closing the input stream + */ + _doneReadingSource(e) { + dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")"); + + this._finishSource(e); + if (this._pendingData.length === 0) { + this._sink = null; + } else { + NS_ASSERT(this._sink !== null, "null output?"); + } + + // If we've written out all data read up to this point, then it's time to + // signal completion. + if (this._sink === null) { + NS_ASSERT(this._pendingData.length === 0, "pending data still?"); + this._cancelOrDispatchCancelCallback(e); + } + }, + + /** + * Stop writing output if we haven't already done so, discard any data that + * remained to be sent, close off input if it wasn't already closed, and kick + * off a copy-completion notice. + * + * @param e : nsresult + * the status to be used when closing input if it wasn't already closed + */ + _doneWritingToSink(e) { + dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")"); + + this._pendingData.length = 0; + this._sink = null; + this._doneReadingSource(e); + }, + + /** + * Completes processing of this copy: either by canceling the copy if it + * hasn't already been canceled using the provided status, or by dispatching + * the cancel callback event (with the originally provided status, of course) + * if it already has been canceled. + * + * @param status : nsresult + * the status code to use to cancel this, if this hasn't already been + * canceled + */ + _cancelOrDispatchCancelCallback(status) { + dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")"); + + NS_ASSERT(this._source === null, "should have finished input"); + NS_ASSERT(this._sink === null, "should have finished output"); + NS_ASSERT(this._pendingData.length === 0, "should have no pending data"); + + if (!this._canceled) { + this.cancel(status); + return; + } + + var self = this; + var event = { + run() { + dumpn("*** onStopRequest async callback"); + + self._completed = true; + try { + self._observer.onStopRequest(self, self.status); + } catch (e) { + NS_ASSERT( + false, + "how are we throwing an exception here? we control " + + "all the callers! " + + e + ); + } + }, + }; + + gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + }, + + /** + * Kicks off another wait for more data to be available from the input stream. + */ + _waitToReadData() { + dumpn("*** _waitToReadData"); + this._source.asyncWait( + this, + 0, + Response.SEGMENT_SIZE, + gThreadManager.mainThread + ); + }, + + /** + * Kicks off another wait until data can be written to the output stream. + */ + _waitToWriteData() { + dumpn("*** _waitToWriteData"); + + var pendingData = this._pendingData; + NS_ASSERT(!!pendingData.length, "no pending data to write?"); + NS_ASSERT(!!pendingData[0].length, "buffered an empty write?"); + + this._sink.asyncWait( + this, + 0, + pendingData[0].length, + gThreadManager.mainThread + ); + }, + + /** + * Kicks off a wait for the sink to which data is being copied to be closed. + * We wait for stream closure when we don't have any data to be copied, rather + * than waiting to write a specific amount of data. We can't wait to write + * data because the sink might be infinitely writable, and if no data appears + * in the source for a long time we might have to spin quite a bit waiting to + * write, waiting to write again, &c. Waiting on stream closure instead means + * we'll get just one notification if the sink dies. Note that when data + * starts arriving from the sink we'll resume waiting for data to be written, + * dropping this closure-only callback entirely. + */ + _waitForSinkClosure() { + dumpn("*** _waitForSinkClosure"); + + this._sink.asyncWait( + this, + Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY, + 0, + gThreadManager.mainThread + ); + }, + + /** + * Closes input with the given status, if it hasn't already been closed; + * otherwise a no-op. + * + * @param status : nsresult + * status code use to close the source stream if necessary + */ + _finishSource(status) { + dumpn("*** _finishSource(" + status.toString(16) + ")"); + + if (this._source !== null) { + this._source.closeWithStatus(status); + this._source = null; + } + }, +}; + +/** + * A container for utility functions used with HTTP headers. + */ +const headerUtils = { + /** + * Normalizes fieldName (by converting it to lowercase) and ensures it is a + * valid header field name (although not necessarily one specified in RFC + * 2616). + * + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not match the field-name production in RFC 2616 + * @returns string + * fieldName converted to lowercase if it is a valid header, for characters + * where case conversion is possible + */ + normalizeFieldName(fieldName) { + if (fieldName == "") { + dumpn("*** Empty fieldName"); + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + for (var i = 0, sz = fieldName.length; i < sz; i++) { + if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)]) { + dumpn(fieldName + " is not a valid header field name!"); + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + } + + return fieldName.toLowerCase(); + }, + + /** + * Ensures that fieldValue is a valid header field value (although not + * necessarily as specified in RFC 2616 if the corresponding field name is + * part of the HTTP protocol), normalizes the value if it is, and + * returns the normalized value. + * + * @param fieldValue : string + * a value to be normalized as an HTTP header field value + * @throws NS_ERROR_INVALID_ARG + * if fieldValue does not match the field-value production in RFC 2616 + * @returns string + * fieldValue as a normalized HTTP header field value + */ + normalizeFieldValue(fieldValue) { + // field-value = *( field-content | LWS ) + // field-content = <the OCTETs making up the field-value + // and consisting of either *TEXT or combinations + // of token, separators, and quoted-string> + // TEXT = <any OCTET except CTLs, + // but including LWS> + // LWS = [CRLF] 1*( SP | HT ) + // + // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + // qdtext = <any TEXT except <">> + // quoted-pair = "\" CHAR + // CHAR = <any US-ASCII character (octets 0 - 127)> + + // Any LWS that occurs between field-content MAY be replaced with a single + // SP before interpreting the field value or forwarding the message + // downstream (section 4.2); we replace 1*LWS with a single SP + var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " "); + + // remove leading/trailing LWS (which has been converted to SP) + val = val.replace(/^ +/, "").replace(/ +$/, ""); + + // that should have taken care of all CTLs, so val should contain no CTLs + dumpn("*** Normalized value: '" + val + "'"); + for (var i = 0, len = val.length; i < len; i++) { + if (isCTL(val.charCodeAt(i))) { + dump("*** Char " + i + " has charcode " + val.charCodeAt(i)); + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + } + + // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly + // normalize, however, so this can be construed as a tightening of the + // spec and not entirely as a bug + return val; + }, +}; + +/** + * Converts the given string into a string which is safe for use in an HTML + * context. + * + * @param str : string + * the string to make HTML-safe + * @returns string + * an HTML-safe version of str + */ +function htmlEscape(str) { + // this is naive, but it'll work + var s = ""; + for (var i = 0; i < str.length; i++) { + s += "&#" + str.charCodeAt(i) + ";"; + } + return s; +} + +/** + * Constructs an object representing an HTTP version (see section 3.1). + * + * @param versionString + * a string of the form "#.#", where # is an non-negative decimal integer with + * or without leading zeros + * @throws + * if versionString does not specify a valid HTTP version number + */ +function nsHttpVersion(versionString) { + var matches = /^(\d+)\.(\d+)$/.exec(versionString); + if (!matches) { + throw new Error("Not a valid HTTP version!"); + } + + /** The major version number of this, as a number. */ + this.major = parseInt(matches[1], 10); + + /** The minor version number of this, as a number. */ + this.minor = parseInt(matches[2], 10); + + if ( + isNaN(this.major) || + isNaN(this.minor) || + this.major < 0 || + this.minor < 0 + ) { + throw new Error("Not a valid HTTP version!"); + } +} +nsHttpVersion.prototype = { + /** + * Returns the standard string representation of the HTTP version represented + * by this (e.g., "1.1"). + */ + toString() { + return this.major + "." + this.minor; + }, + + /** + * Returns true if this represents the same HTTP version as otherVersion, + * false otherwise. + * + * @param otherVersion : nsHttpVersion + * the version to compare against this + */ + equals(otherVersion) { + return this.major == otherVersion.major && this.minor == otherVersion.minor; + }, + + /** True if this >= otherVersion, false otherwise. */ + atLeast(otherVersion) { + return ( + this.major > otherVersion.major || + (this.major == otherVersion.major && this.minor >= otherVersion.minor) + ); + }, +}; + +nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0"); +nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1"); + +/** + * An object which stores HTTP headers for a request or response. + * + * Note that since headers are case-insensitive, this object converts headers to + * lowercase before storing them. This allows the getHeader and hasHeader + * methods to work correctly for any case of a header, but it means that the + * values returned by .enumerator may not be equal case-sensitively to the + * values passed to setHeader when adding headers to this. + */ +function nsHttpHeaders() { + /** + * A hash of headers, with header field names as the keys and header field + * values as the values. Header field names are case-insensitive, but upon + * insertion here they are converted to lowercase. Header field values are + * normalized upon insertion to contain no leading or trailing whitespace. + * + * Note also that per RFC 2616, section 4.2, two headers with the same name in + * a message may be treated as one header with the same field name and a field + * value consisting of the separate field values joined together with a "," in + * their original order. This hash stores multiple headers with the same name + * in this manner. + */ + this._headers = {}; +} +nsHttpHeaders.prototype = { + /** + * Sets the header represented by name and value in this. + * + * @param name : string + * the header name + * @param value : string + * the header value + * @throws NS_ERROR_INVALID_ARG + * if name or value is not a valid header component + */ + setHeader(fieldName, fieldValue, merge) { + var name = headerUtils.normalizeFieldName(fieldName); + var value = headerUtils.normalizeFieldValue(fieldValue); + + // The following three headers are stored as arrays because their real-world + // syntax prevents joining individual headers into a single header using + // ",". See also <https://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77> + if (merge && name in this._headers) { + if ( + name === "www-authenticate" || + name === "proxy-authenticate" || + name === "set-cookie" + ) { + this._headers[name].push(value); + } else { + this._headers[name][0] += "," + value; + NS_ASSERT( + this._headers[name].length === 1, + "how'd a non-special header have multiple values?" + ); + } + } else { + this._headers[name] = [value]; + } + }, + + setHeaderNoCheck(fieldName, fieldValue) { + var name = headerUtils.normalizeFieldName(fieldName); + var value = headerUtils.normalizeFieldValue(fieldValue); + if (name in this._headers) { + this._headers[name].push(value); + } else { + this._headers[name] = [value]; + } + }, + + /** + * Returns the value for the header specified by this. + * + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not constitute a valid header field name + * @throws NS_ERROR_NOT_AVAILABLE + * if the given header does not exist in this + * @returns string + * the field value for the given header, possibly with non-semantic changes + * (i.e., leading/trailing whitespace stripped, whitespace runs replaced + * with spaces, etc.) at the option of the implementation; multiple + * instances of the header will be combined with a comma, except for + * the three headers noted in the description of getHeaderValues + */ + getHeader(fieldName) { + return this.getHeaderValues(fieldName).join("\n"); + }, + + /** + * Returns the value for the header specified by fieldName as an array. + * + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not constitute a valid header field name + * @throws NS_ERROR_NOT_AVAILABLE + * if the given header does not exist in this + * @returns [string] + * an array of all the header values in this for the given + * header name. Header values will generally be collapsed + * into a single header by joining all header values together + * with commas, but certain headers (Proxy-Authenticate, + * WWW-Authenticate, and Set-Cookie) violate the HTTP spec + * and cannot be collapsed in this manner. For these headers + * only, the returned array may contain multiple elements if + * that header has been added more than once. + */ + getHeaderValues(fieldName) { + var name = headerUtils.normalizeFieldName(fieldName); + + if (name in this._headers) { + return this._headers[name]; + } + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + }, + + /** + * Returns true if a header with the given field name exists in this, false + * otherwise. + * + * @param fieldName : string + * the field name whose existence is to be determined in this + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not constitute a valid header field name + * @returns boolean + * true if the header's present, false otherwise + */ + hasHeader(fieldName) { + var name = headerUtils.normalizeFieldName(fieldName); + return name in this._headers; + }, + + /** + * Returns a new enumerator over the field names of the headers in this, as + * nsISupportsStrings. The names returned will be in lowercase, regardless of + * how they were input using setHeader (header names are case-insensitive per + * RFC 2616). + */ + get enumerator() { + var headers = []; + for (var i in this._headers) { + var supports = new SupportsString(); + supports.data = i; + headers.push(supports); + } + + return new nsSimpleEnumerator(headers); + }, +}; + +/** + * Constructs an nsISimpleEnumerator for the given array of items. + * + * @param items : Array + * the items, which must all implement nsISupports + */ +function nsSimpleEnumerator(items) { + this._items = items; + this._nextIndex = 0; +} +nsSimpleEnumerator.prototype = { + hasMoreElements() { + return this._nextIndex < this._items.length; + }, + getNext() { + if (!this.hasMoreElements()) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + + return this._items[this._nextIndex++]; + }, + [Symbol.iterator]() { + return this._items.values(); + }, + QueryInterface: ChromeUtils.generateQI(["nsISimpleEnumerator"]), +}; + +/** + * A representation of the data in an HTTP request. + * + * @param port : uint + * the port on which the server receiving this request runs + */ +function Request(port) { + /** Method of this request, e.g. GET or POST. */ + this._method = ""; + + /** Path of the requested resource; empty paths are converted to '/'. */ + this._path = ""; + + /** Query string, if any, associated with this request (not including '?'). */ + this._queryString = ""; + + /** Scheme of requested resource, usually http, always lowercase. */ + this._scheme = "http"; + + /** Hostname on which the requested resource resides. */ + this._host = undefined; + + /** Port number over which the request was received. */ + this._port = port; + + var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null); + + /** Stream from which data in this request's body may be read. */ + this._bodyInputStream = bodyPipe.inputStream; + + /** Stream to which data in this request's body is written. */ + this._bodyOutputStream = bodyPipe.outputStream; + + /** + * The headers in this request. + */ + this._headers = new nsHttpHeaders(); + + /** + * For the addition of ad-hoc properties and new functionality without having + * to change nsIHttpRequest every time; currently lazily created, as its only + * use is in directory listings. + */ + this._bag = null; +} +Request.prototype = { + // SERVER METADATA + + // + // see nsIHttpRequest.scheme + // + get scheme() { + return this._scheme; + }, + + // + // see nsIHttpRequest.host + // + get host() { + return this._host; + }, + + // + // see nsIHttpRequest.port + // + get port() { + return this._port; + }, + + // REQUEST LINE + + // + // see nsIHttpRequest.method + // + get method() { + return this._method; + }, + + // + // see nsIHttpRequest.httpVersion + // + get httpVersion() { + return this._httpVersion.toString(); + }, + + // + // see nsIHttpRequest.path + // + get path() { + return this._path; + }, + + // + // see nsIHttpRequest.queryString + // + get queryString() { + return this._queryString; + }, + + // HEADERS + + // + // see nsIHttpRequest.getHeader + // + getHeader(name) { + return this._headers.getHeader(name); + }, + + // + // see nsIHttpRequest.hasHeader + // + hasHeader(name) { + return this._headers.hasHeader(name); + }, + + // + // see nsIHttpRequest.headers + // + get headers() { + return this._headers.enumerator; + }, + + // + // see nsIPropertyBag.enumerator + // + get enumerator() { + this._ensurePropertyBag(); + return this._bag.enumerator; + }, + + // + // see nsIHttpRequest.headers + // + get bodyInputStream() { + return this._bodyInputStream; + }, + + // + // see nsIPropertyBag.getProperty + // + getProperty(name) { + this._ensurePropertyBag(); + return this._bag.getProperty(name); + }, + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: ChromeUtils.generateQI(["nsIHttpRequest"]), + + // PRIVATE IMPLEMENTATION + + /** Ensures a property bag has been created for ad-hoc behaviors. */ + _ensurePropertyBag() { + if (!this._bag) { + this._bag = new WritablePropertyBag(); + } + }, +}; + +/** + * Creates a new HTTP server listening for loopback traffic on the given port, + * starts it, and runs the server until the server processes a shutdown request, + * spinning an event loop so that events posted by the server's socket are + * processed. + * + * This method is primarily intended for use in running this script from within + * xpcshell and running a functional HTTP server without having to deal with + * non-essential details. + * + * Note that running multiple servers using variants of this method probably + * doesn't work, simply due to how the internal event loop is spun and stopped. + * + * @note + * This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code); + * you should use this server as a component in Mozilla 1.8. + * @param port + * the port on which the server will run, or -1 if there exists no preference + * for a specific port; note that attempting to use some values for this + * parameter (particularly those below 1024) may cause this method to throw or + * may result in the server being prematurely shut down + * @param basePath + * a local directory from which requests will be served (i.e., if this is + * "/home/jwalden/" then a request to /index.html will load + * /home/jwalden/index.html); if this is omitted, only the default URLs in + * this server implementation will be functional + */ +function server(port, basePath) { + if (basePath) { + var lp = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + lp.initWithPath(basePath); + } + + // if you're running this, you probably want to see debugging info + DEBUG = true; + + var srv = new nsHttpServer(); + if (lp) { + srv.registerDirectory("/", lp); + } + srv.registerContentType("sjs", SJS_TYPE); + srv.identity.setPrimary("http", "localhost", port); + srv.start(port); + + var thread = gThreadManager.currentThread; + while (!srv.isStopped()) { + thread.processNextEvent(true); + } + + // get rid of any pending requests + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + + DEBUG = false; +} diff --git a/netwerk/test/httpserver/moz.build b/netwerk/test/httpserver/moz.build new file mode 100644 index 0000000000..4f5260ff79 --- /dev/null +++ b/netwerk/test/httpserver/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPIDL_SOURCES += [ + "nsIHttpServer.idl", +] + +XPIDL_MODULE = "test_necko" + +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell.ini"] + +EXTRA_COMPONENTS += [ + "httpd.js", +] + +TESTING_JS_MODULES += [ + "httpd.js", +] diff --git a/netwerk/test/httpserver/nsIHttpServer.idl b/netwerk/test/httpserver/nsIHttpServer.idl new file mode 100644 index 0000000000..83614dbdb0 --- /dev/null +++ b/netwerk/test/httpserver/nsIHttpServer.idl @@ -0,0 +1,649 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIInputStream; +interface nsIFile; +interface nsIOutputStream; +interface nsISimpleEnumerator; + +interface nsIHttpServer; +interface nsIHttpServerStoppedCallback; +interface nsIHttpRequestHandler; +interface nsIHttpRequest; +interface nsIHttpResponse; +interface nsIHttpServerIdentity; + +/** + * An interface which represents an HTTP server. + */ +[scriptable, uuid(cea8812e-faa6-4013-9396-f9936cbb74ec)] +interface nsIHttpServer : nsISupports +{ + /** + * Starts up this server, listening upon the given port. + * + * @param port + * the port upon which listening should happen, or -1 if no specific port is + * desired + * @throws NS_ERROR_ALREADY_INITIALIZED + * if this server is already started + * @throws NS_ERROR_NOT_AVAILABLE + * if the server is not started and cannot be started on the desired port + * (perhaps because the port is already in use or because the process does + * not have privileges to do so) + * @note + * Behavior is undefined if this method is called after stop() has been + * called on this but before the provided callback function has been + * called. + */ + void start(in long port); + + /** + * Starts up this server, listening upon the given port on a ipv6 adddress. + * + * @param port + * the port upon which listening should happen, or -1 if no specific port is + * desired + * @throws NS_ERROR_ALREADY_INITIALIZED + * if this server is already started + * @throws NS_ERROR_NOT_AVAILABLE + * if the server is not started and cannot be started on the desired port + * (perhaps because the port is already in use or because the process does + * not have privileges to do so) + * @note + * Behavior is undefined if this method is called after stop() has been + * called on this but before the provided callback function has been + * called. + */ + void start_ipv6(in long port); + + /** + * Like the two functions above, but this server supports both IPv6 and + * IPv4 addresses. + */ + void start_dualStack(in long port); + + /** + * Shuts down this server if it is running (including the period of time after + * stop() has been called but before the provided callback has been called). + * + * @param callback + * an asynchronous callback used to notify the user when this server is + * stopped and all pending requests have been fully served + * @throws NS_ERROR_NULL_POINTER + * if callback is null + * @throws NS_ERROR_UNEXPECTED + * if this server is not running + */ + void stop(in nsIHttpServerStoppedCallback callback); + + /** + * Associates the local file represented by the string file with all requests + * which match request. + * + * @param path + * the path which is to be mapped to the given file; must begin with "/" and + * be a valid URI path (i.e., no query string, hash reference, etc.) + * @param file + * the file to serve for the given path, or null to remove any mapping that + * might exist; this file must exist for the lifetime of the server + * @param handler + * an optional object which can be used to handle any further changes. + */ + void registerFile(in string path, + in nsIFile file, + [optional] in nsIHttpRequestHandler handler); + + /** + * Registers a custom path handler. + * + * @param path + * the path on the server (beginning with a "/") which is to be handled by + * handler; this path must not include a query string or hash component; it + * also should usually be canonicalized, since most browsers will do so + * before sending otherwise-matching requests + * @param handler + * an object which will handle any requests for the given path, or null to + * remove any existing handler; if while the server is running the handler + * throws an exception while responding to a request, an HTTP 500 response + * will be returned + * @throws NS_ERROR_INVALID_ARG + * if path does not begin with a "/" + */ + void registerPathHandler(in string path, in nsIHttpRequestHandler handler); + + /** + * Registers a custom prefix handler. + * + * @param prefix + * the path on the server (beginning and ending with "/") which is to be + * handled by handler; this path must not include a query string or hash + * component. All requests that start with this prefix will be directed to + * the given handler. + * @param handler + * an object which will handle any requests for the given path, or null to + * remove any existing handler; if while the server is running the handler + * throws an exception while responding to a request, an HTTP 500 response + * will be returned + * @throws NS_ERROR_INVALID_ARG + * if path does not begin with a "/" or does not end with a "/" + */ + void registerPrefixHandler(in string prefix, in nsIHttpRequestHandler handler); + + /** + * Registers a custom error page handler. + * + * @param code + * the error code which is to be handled by handler + * @param handler + * an object which will handle any requests which generate the given status + * code, or null to remove any existing handler. If the handler throws an + * exception during server operation, fallback is to the genericized error + * handler (the x00 version), then to 500, using a user-defined error + * handler if one exists or the server default handler otherwise. Fallback + * will never occur from a user-provided handler that throws to the same + * handler as provided by the server, e.g. a throwing user 404 falls back to + * 400, not a server-provided 404 that might not throw. + * @note + * If the error handler handles HTTP 500 and throws, behavior is undefined. + */ + void registerErrorHandler(in unsigned long code, in nsIHttpRequestHandler handler); + + /** + * Maps all requests to paths beneath path to the corresponding file beneath + * dir. + * + * @param path + * the absolute path on the server against which requests will be served + * from dir (e.g., "/", "/foo/", etc.); must begin and end with a forward + * slash + * @param dir + * the directory to be used to serve all requests for paths underneath path + * (except those further overridden by another, deeper path registered with + * another directory); if null, any current mapping for the given path is + * removed + * @throws NS_ERROR_INVALID_ARG + * if dir is non-null and does not exist or is not a directory, or if path + * does not begin with and end with a forward slash + */ + void registerDirectory(in string path, in nsIFile dir); + + /** + * Associates files with the given extension with the given Content-Type when + * served by this server, in the absence of any file-specific information + * about the desired Content-Type. If type is empty, removes any extant + * mapping, if one is present. + * + * @throws NS_ERROR_INVALID_ARG + * if the given type is not a valid header field value, i.e. if it doesn't + * match the field-value production in RFC 2616 + * @note + * No syntax checking is done of the given type, beyond ensuring that it is + * a valid header field value. Behavior when not given a string matching + * the media-type production in RFC 2616 section 3.7 is undefined. + * Implementations may choose to define specific behavior for types which do + * not match the production, such as for CGI functionality. + * @note + * Implementations MAY treat type as a trusted argument; users who fail to + * generate this string from trusted data risk security vulnerabilities. + */ + void registerContentType(in string extension, in string type); + + /** + * Sets the handler used to display the contents of a directory if + * the directory contains no index page. + * + * @param handler + * an object which will handle any requests for directories which + * do not contain index pages, or null to reset to the default + * index handler; if while the server is running the handler + * throws an exception while responding to a request, an HTTP 500 + * response will be returned. An nsIFile corresponding to the + * directory is available from the metadata object passed to the + * handler, under the key "directory". + */ + void setIndexHandler(in nsIHttpRequestHandler handler); + + /** Represents the locations at which this server is reachable. */ + readonly attribute nsIHttpServerIdentity identity; + + /** + * Retrieves the string associated with the given key in this, for the given + * path's saved state. All keys are initially associated with the empty + * string. + */ + AString getState(in AString path, in AString key); + + /** + * Sets the string associated with the given key in this, for the given path's + * saved state. + */ + void setState(in AString path, in AString key, in AString value); + + /** + * Retrieves the string associated with the given key in this, in + * entire-server saved state. All keys are initially associated with the + * empty string. + */ + AString getSharedState(in AString key); + + /** + * Sets the string associated with the given key in this, in entire-server + * saved state. + */ + void setSharedState(in AString key, in AString value); + + /** + * Retrieves the object associated with the given key in this in + * object-valued saved state. All keys are initially associated with null. + */ + nsISupports getObjectState(in AString key); + + /** + * Sets the object associated with the given key in this in object-valued + * saved state. The value may be null. + */ + void setObjectState(in AString key, in nsISupports value); +}; + +/** + * An interface through which a notification of the complete stopping (socket + * closure, in-flight requests all fully served and responded to) of an HTTP + * server may be received. + */ +[scriptable, function, uuid(925a6d33-9937-4c63-abe1-a1c56a986455)] +interface nsIHttpServerStoppedCallback : nsISupports +{ + /** Called when the corresponding server has been fully stopped. */ + void onStopped(); +}; + +/** + * Represents a set of names for a server, one of which is the primary name for + * the server and the rest of which are secondary. By default every server will + * contain ("http", "localhost", port) and ("http", "127.0.0.1", port) as names, + * where port is what was provided to the corresponding server when started; + * however, except for their being removed when the corresponding server stops + * they have no special importance. + */ +[scriptable, uuid(a89de175-ae8e-4c46-91a5-0dba99bbd284)] +interface nsIHttpServerIdentity : nsISupports +{ + /** + * The primary scheme at which the corresponding server is located, defaulting + * to 'http'. This name will be the value of nsIHttpRequest.scheme for + * HTTP/1.0 requests. + * + * This value is always set when the corresponding server is running. If the + * server is not running, this value is set only if it has been set to a + * non-default name using setPrimary. In this case reading this value will + * throw NS_ERROR_NOT_INITIALIZED. + */ + readonly attribute string primaryScheme; + + /** + * The primary name by which the corresponding server is known, defaulting to + * 'localhost'. This name will be the value of nsIHttpRequest.host for + * HTTP/1.0 requests. + * + * This value is always set when the corresponding server is running. If the + * server is not running, this value is set only if it has been set to a + * non-default name using setPrimary. In this case reading this value will + * throw NS_ERROR_NOT_INITIALIZED. + */ + readonly attribute string primaryHost; + + /** + * The primary port on which the corresponding server runs, defaulting to the + * associated server's port. This name will be the value of + * nsIHttpRequest.port for HTTP/1.0 requests. + * + * This value is always set when the corresponding server is running. If the + * server is not running, this value is set only if it has been set to a + * non-default name using setPrimary. In this case reading this value will + * throw NS_ERROR_NOT_INITIALIZED. + */ + readonly attribute long primaryPort; + + /** + * Adds a location at which this server may be accessed. + * + * @throws NS_ERROR_ILLEGAL_VALUE + * if scheme or host do not match the scheme or host productions imported + * into RFC 2616 from RFC 2396, or if port is not a valid port number + */ + void add(in string scheme, in string host, in long port); + + /** + * Removes this name from the list of names by which the corresponding server + * is known. If name is also the primary name for the server, the primary + * name reverts to 'http://127.0.0.1' with the associated server's port. + * + * @throws NS_ERROR_ILLEGAL_VALUE + * if scheme or host do not match the scheme or host productions imported + * into RFC 2616 from RFC 2396, or if port is not a valid port number + * @returns + * true if the given name was a name for this server, false otherwise + */ + boolean remove(in string scheme, in string host, in long port); + + /** + * Returns true if the given name is in this, false otherwise. + * + * @throws NS_ERROR_ILLEGAL_VALUE + * if scheme or host do not match the scheme or host productions imported + * into RFC 2616 from RFC 2396, or if port is not a valid port number + */ + boolean has(in string scheme, in string host, in long port); + + /** + * Returns the scheme for the name with the given host and port, if one is + * present; otherwise returns the empty string. + * + * @throws NS_ERROR_ILLEGAL_VALUE + * if host does not match the host production imported into RFC 2616 from + * RFC 2396, or if port is not a valid port number + */ + string getScheme(in string host, in long port); + + /** + * Designates the given name as the primary name in this and adds it to this + * if it is not already present. + * + * @throws NS_ERROR_ILLEGAL_VALUE + * if scheme or host do not match the scheme or host productions imported + * into RFC 2616 from RFC 2396, or if port is not a valid port number + */ + void setPrimary(in string scheme, in string host, in long port); +}; + +/** + * A representation of a handler for HTTP requests. The handler is used by + * calling its .handle method with data for an incoming request; it is the + * handler's job to use that data as it sees fit to make the desired response. + * + * @note + * This interface uses the [function] attribute, so you can pass a + * script-defined function with the functionality of handle() to any + * method which has a nsIHttpRequestHandler parameter, instead of wrapping + * it in an otherwise empty object. + */ +[scriptable, function, uuid(2bbb4db7-d285-42b3-a3ce-142b8cc7e139)] +interface nsIHttpRequestHandler : nsISupports +{ + /** + * Processes an HTTP request and initializes the passed-in response to reflect + * the correct HTTP response. + * + * If this method throws an exception, externally observable behavior depends + * upon whether is being processed asynchronously. If such is the case, the + * output is some prefix (perhaps all, perhaps none, perhaps only some) of the + * data which would have been sent if, instead, the response had been finished + * at that point. If no data has been written, the response has not had + * seizePower() called on it, and it is not being asynchronously created, an + * error handler will be invoked (usually 500 unless otherwise specified). + * + * Some uses of nsIHttpRequestHandler may require this method to never throw + * an exception; in the general case, however, this method may throw an + * exception (causing an HTTP 500 response to occur, if the above conditions + * are met). + * + * @param request + * data representing an HTTP request + * @param response + * an initially-empty response which must be modified to reflect the data + * which should be sent as the response to the request described by metadata + */ + void handle(in nsIHttpRequest request, in nsIHttpResponse response); +}; + + +/** + * A representation of the data included in an HTTP request. + */ +[scriptable, uuid(978cf30e-ad73-42ee-8f22-fe0aaf1bf5d2)] +interface nsIHttpRequest : nsISupports +{ + /** + * The request type for this request (see RFC 2616, section 5.1.1). + */ + readonly attribute string method; + + /** + * The scheme of the requested path, usually 'http' but might possibly be + * 'https' if some form of SSL tunneling is in use. Note that this value + * cannot be accurately determined unless the incoming request used the + * absolute-path form of the request line; it defaults to 'http', so only + * if it is something else can you be entirely certain it's correct. + */ + readonly attribute string scheme; + + /** + * The host of the data being requested (e.g. "localhost" for the + * http://localhost:8080/file resource). Note that the relevant port on the + * host is specified in this.port. This value is in the ASCII character + * encoding. + */ + readonly attribute string host; + + /** + * The port on the server on which the request was received. + */ + readonly attribute unsigned long port; + + /** + * The requested path, without any query string (e.g. "/dir/file.txt"). It is + * guaranteed to begin with a "/". The individual components in this string + * are URL-encoded. + */ + readonly attribute string path; + + /** + * The URL-encoded query string associated with this request, not including + * the initial "?", or "" if no query string was present. + */ + readonly attribute string queryString; + + /** + * A string containing the HTTP version of the request (i.e., "1.1"). Leading + * zeros for either component of the version will be omitted. (In other + * words, if the request contains the version "1.01", this attribute will be + * "1.1"; see RFC 2616, section 3.1.) + */ + readonly attribute string httpVersion; + + /** + * Returns the value for the header in this request specified by fieldName. + * + * @param fieldName + * the name of the field whose value is to be gotten; note that since HTTP + * header field names are case-insensitive, this method produces equivalent + * results for "HeAdER" and "hEADer" as fieldName + * @returns + * The result is a string containing the individual values of the header, + * usually separated with a comma. The headers WWW-Authenticate, + * Proxy-Authenticate, and Set-Cookie violate the HTTP specification, + * however, and for these headers only the separator string is '\n'. + * + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not constitute a valid header field name + * @throws NS_ERROR_NOT_AVAILABLE + * if the given header does not exist in this + */ + string getHeader(in string fieldName); + + /** + * Returns true if a header with the given field name exists in this, false + * otherwise. + * + * @param fieldName + * the field name whose existence is to be determined in this; note that + * since HTTP header field names are case-insensitive, this method produces + * equivalent results for "HeAdER" and "hEADer" as fieldName + * @throws NS_ERROR_INVALID_ARG + * if fieldName does not constitute a valid header field name + */ + boolean hasHeader(in string fieldName); + + /** + * An nsISimpleEnumerator of nsISupportsStrings over the names of the headers + * in this request. The header field names in the enumerator may not + * necessarily have the same case as they do in the request itself. + */ + readonly attribute nsISimpleEnumerator headers; + + /** + * A stream from which data appearing in the body of this request can be read. + */ + readonly attribute nsIInputStream bodyInputStream; +}; + + +/** + * Represents an HTTP response, as described in RFC 2616, section 6. + */ +[scriptable, uuid(1acd16c2-dc59-42fa-9160-4f26c43c1c21)] +interface nsIHttpResponse : nsISupports +{ + /** + * Sets the status line for this. If this method is never called on this, the + * status line defaults to "HTTP/", followed by the server's default HTTP + * version (e.g. "1.1"), followed by " 200 OK". + * + * @param httpVersion + * the HTTP version of this, as a string (e.g. "1.1"); if null, the server + * default is used + * @param code + * the numeric HTTP status code for this + * @param description + * a human-readable description of code; may be null if no description is + * desired + * @throws NS_ERROR_INVALID_ARG + * if httpVersion is not a valid HTTP version string, statusCode is greater + * than 999, or description contains invalid characters + * @throws NS_ERROR_NOT_AVAILABLE + * if this response is being processed asynchronously and data has been + * written to this response's body, or if seizePower() has been called on + * this + */ + void setStatusLine(in string httpVersion, + in unsigned short statusCode, + in string description); + + /** + * Sets the specified header in this. + * + * @param name + * the name of the header; must match the field-name production per RFC 2616 + * @param value + * the value of the header; must match the field-value production per RFC + * 2616 + * @param merge + * when true, if the given header already exists in this, the values passed + * to this function will be merged into the existing header, per RFC 2616 + * header semantics (except for the Set-Cookie, WWW-Authenticate, and + * Proxy-Authenticate headers, which will treat each such merged header as + * an additional instance of the header, for real-world compatibility + * reasons); when false, replaces any existing header of the given name (if + * any exists) with a new header with the specified value + * @throws NS_ERROR_INVALID_ARG + * if name or value is not a valid header component + * @throws NS_ERROR_NOT_AVAILABLE + * if this response is being processed asynchronously and data has been + * written to this response's body, or if seizePower() has been called on + * this + */ + void setHeader(in string name, in string value, in boolean merge); + + /** + * This is used for testing our header handling, so header will be sent out + * without transformation. There can be multiple headers. + */ + void setHeaderNoCheck(in string name, in string value); + + /** + * A stream to which data appearing in the body of this response (or in the + * totality of the response if seizePower() is called) should be written. + * After this response has been designated as being processed asynchronously, + * or after seizePower() has been called on this, subsequent writes will no + * longer be buffered and will be written to the underlying transport without + * delaying until the entire response is constructed. Write-through may or + * may not be synchronous in the implementation, and in any case particular + * behavior may not be observable to the HTTP client as intermediate buffers + * both in the server socket and in the client may delay written data; be + * prepared for delays at any time. + * + * @throws NS_ERROR_NOT_AVAILABLE + * if accessed after this response is fully constructed + */ + readonly attribute nsIOutputStream bodyOutputStream; + + /** + * Writes a string to the response's output stream. This method is merely a + * convenient shorthand for writing the same data to bodyOutputStream + * directly. + * + * @note + * This method is only guaranteed to work with ASCII data. + * @throws NS_ERROR_NOT_AVAILABLE + * if called after this response has been fully constructed + */ + void write(in string data); + + /** + * Signals that this response is being constructed asynchronously. Requests + * are typically completely constructed during nsIHttpRequestHandler.handle; + * however, responses which require significant resources (time, memory, + * processing) to construct can be created and sent incrementally by calling + * this method during the call to nsIHttpRequestHandler.handle. This method + * only has this effect when called during nsIHttpRequestHandler.handle; + * behavior is undefined if it is called at a later time. It may be called + * multiple times with no ill effect, so long as each call occurs before + * finish() is called. + * + * @throws NS_ERROR_UNEXPECTED + * if not initially called within a nsIHttpRequestHandler.handle call or if + * called after this response has been finished + * @throws NS_ERROR_NOT_AVAILABLE + * if seizePower() has been called on this + */ + void processAsync(); + + /** + * Seizes complete control of this response (and its connection) from the + * server, allowing raw and unfettered access to data being sent in the HTTP + * response. Once this method has been called the only property which may be + * accessed without an exception being thrown is bodyOutputStream, and the + * only methods which may be accessed without an exception being thrown are + * write(), finish(), and seizePower() (which may be called multiple times + * without ill effect so long as all calls are otherwise allowed). + * + * After a successful call, all data subsequently written to the body of this + * response is written directly to the corresponding connection. (Previously- + * written data is silently discarded.) No status line or headers are sent + * before doing so; if the response handler wishes to write such data, it must + * do so manually. Data generation completes only when finish() is called; it + * is not enough to simply call close() on bodyOutputStream. + * + * @throws NS_ERROR_NOT_AVAILABLE + * if processAsync() has been called on this + * @throws NS_ERROR_UNEXPECTED + * if finish() has been called on this + */ + void seizePower(); + + /** + * Signals that construction of this response is complete and that it may be + * sent over the network to the client, or if seizePower() has been called + * signals that all data has been written and that the underlying connection + * may be closed. This method may only be called after processAsync() or + * seizePower() has been called. This method is idempotent. + * + * @throws NS_ERROR_UNEXPECTED + * if processAsync() or seizePower() has not already been properly called + */ + void finish(); +}; diff --git a/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^ new file mode 100644 index 0000000000..b005a65fd2 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^ @@ -0,0 +1 @@ +If this has goofy headers on it, it's a success. diff --git a/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^ new file mode 100644 index 0000000000..66e1522317 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/caret_test.txt^^headers^ @@ -0,0 +1,3 @@ +HTTP 500 This Isn't A Server Error +Foo-RFC: 3092 +Shaving-Cream-Atom: Illudium Phosdex diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_both.html b/netwerk/test/httpserver/test/data/cern_meta/test_both.html new file mode 100644 index 0000000000..db18ea5d7a --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_both.html @@ -0,0 +1,2 @@ +This page is a text file served with status 501. (That's really a lie, tho, +because this is definitely Implemented.) diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^ new file mode 100644 index 0000000000..bb3c16a2e2 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_both.html^headers^ @@ -0,0 +1,2 @@ +HTTP 501 Unimplemented +Content-Type: text/plain diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt new file mode 100644 index 0000000000..7235fa32a5 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt @@ -0,0 +1,9 @@ +<html> +<head> + <title>This is really HTML, not text</title> +</head> +<body> +<p>This file is really HTML; the test_ctype_override.txt^headers^ file sets a + new header that overwrites the default text/plain header.</p> +</body> +</html> diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^ new file mode 100644 index 0000000000..156209f9c8 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_ctype_override.txt^headers^ @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html new file mode 100644 index 0000000000..fd243c640e --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html @@ -0,0 +1,9 @@ +<html> +<head> + <title>This is a 404 page</title> +</head> +<body> +<p>This page has a 404 HTTP status associated with it, via + <code>test_status_override.html^headers^</code>.</p> +</body> +</html> diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^ new file mode 100644 index 0000000000..f438a05746 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override.html^headers^ @@ -0,0 +1 @@ +HTTP 404 Can't Find This diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt new file mode 100644 index 0000000000..4718ec282f --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt @@ -0,0 +1 @@ +This page has an HTTP status override without a description (it defaults to ""). diff --git a/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^ new file mode 100644 index 0000000000..32da7632f9 --- /dev/null +++ b/netwerk/test/httpserver/test/data/cern_meta/test_status_override_nodesc.txt^headers^ @@ -0,0 +1 @@ +HTTP 732 diff --git a/netwerk/test/httpserver/test/data/name-scheme/bar.html^^ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^ new file mode 100644 index 0000000000..bed1f34c9f --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^ @@ -0,0 +1,10 @@ +<html> +<head> + <title>Welcome to bar.html^</title> +</head> +<body> +<p>This file is named with two trailing carets, so the last is stripped + away, producing bar.html^ as the final name.</p> +</body> +</html> + diff --git a/netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^ new file mode 100644 index 0000000000..04fbaa08fe --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/bar.html^^headers^ @@ -0,0 +1,2 @@ +HTTP 200 OK +Content-Type: text/html diff --git a/netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^ new file mode 100644 index 0000000000..dccee48e34 --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/ERROR_IF_SEE_THIS.txt^ @@ -0,0 +1 @@ +This file shouldn't be shown in directory listings. diff --git a/netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^ new file mode 100644 index 0000000000..a8ee35a3b6 --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/SHOULD_SEE_THIS.txt^^ @@ -0,0 +1 @@ +This file should show up in directory listings as SHOULD_SEE_THIS.txt^. diff --git a/netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt b/netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt new file mode 100644 index 0000000000..2ceca8ca9e --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/folder^^/file.txt @@ -0,0 +1,2 @@ +File in a directory named with a trailing caret (in the virtual FS; on disk it +actually ends with two carets). diff --git a/netwerk/test/httpserver/test/data/name-scheme/foo.html^ b/netwerk/test/httpserver/test/data/name-scheme/foo.html^ new file mode 100644 index 0000000000..a3efe8b5c3 --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/foo.html^ @@ -0,0 +1,9 @@ +<html> +<head> + <title>ERROR</title> +</head> +<body> +<p>This file should never be served by the web server because its name ends + with a caret not followed by another caret.</p> +</body> +</html> diff --git a/netwerk/test/httpserver/test/data/name-scheme/normal-file.txt b/netwerk/test/httpserver/test/data/name-scheme/normal-file.txt new file mode 100644 index 0000000000..ab71eabaf0 --- /dev/null +++ b/netwerk/test/httpserver/test/data/name-scheme/normal-file.txt @@ -0,0 +1 @@ +This should be seen. diff --git a/netwerk/test/httpserver/test/data/ranges/empty.txt b/netwerk/test/httpserver/test/data/ranges/empty.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/netwerk/test/httpserver/test/data/ranges/empty.txt diff --git a/netwerk/test/httpserver/test/data/ranges/headers.txt b/netwerk/test/httpserver/test/data/ranges/headers.txt new file mode 100644 index 0000000000..6cf83528c8 --- /dev/null +++ b/netwerk/test/httpserver/test/data/ranges/headers.txt @@ -0,0 +1 @@ +Hello Kitty diff --git a/netwerk/test/httpserver/test/data/ranges/headers.txt^headers^ b/netwerk/test/httpserver/test/data/ranges/headers.txt^headers^ new file mode 100644 index 0000000000..d0a633f042 --- /dev/null +++ b/netwerk/test/httpserver/test/data/ranges/headers.txt^headers^ @@ -0,0 +1 @@ +X-SJS-Header: customized diff --git a/netwerk/test/httpserver/test/data/ranges/range.txt b/netwerk/test/httpserver/test/data/ranges/range.txt new file mode 100644 index 0000000000..ab71eabaf0 --- /dev/null +++ b/netwerk/test/httpserver/test/data/ranges/range.txt @@ -0,0 +1 @@ +This should be seen. diff --git a/netwerk/test/httpserver/test/data/sjs/cgi.sjs b/netwerk/test/httpserver/test/data/sjs/cgi.sjs new file mode 100644 index 0000000000..a6b987a8b7 --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/cgi.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) { + if (request.queryString == "throw") { + throw new Error("monkey wrench!"); + } + + response.setHeader("Content-Type", "text/plain", false); + response.write("PASS"); +} diff --git a/netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^ b/netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^ new file mode 100644 index 0000000000..a83ff774ab --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/cgi.sjs^headers^ @@ -0,0 +1,2 @@ +HTTP 500 Error +This-Header: SHOULD NOT APPEAR IN CGI.JSC RESPONSES! diff --git a/netwerk/test/httpserver/test/data/sjs/object-state.sjs b/netwerk/test/httpserver/test/data/sjs/object-state.sjs new file mode 100644 index 0000000000..8f027dfedf --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/object-state.sjs @@ -0,0 +1,74 @@ +function parseQueryString(str) { + var paramArray = str.split("&"); + var regex = /^([^=]+)=(.*)$/; + var params = {}; + for (var i = 0, sz = paramArray.length; i < sz; i++) { + var match = regex.exec(paramArray[i]); + if (!match) { + throw new Error("Bad parameter in queryString! '" + paramArray[i] + "'"); + } + params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); + } + + return params; +} + +/* + * We're relying somewhat dubiously on all data being sent as soon as it's + * available at numerous levels (in Necko in the server-side part of the + * connection, in the OS's outgoing socket buffer, in the OS's incoming socket + * buffer, and in Necko in the client-side part of the connection), but to the + * best of my knowledge there's no way to force data flow at all those levels, + * so this is the best we can do. + */ +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + /* + * NB: A Content-Type header is *necessary* to avoid content-sniffing, which + * will delay onStartRequest past the the point where the entire head of + * the response has been received. + */ + response.setHeader("Content-Type", "text/plain", false); + + var params = parseQueryString(request.queryString); + + switch (params.state) { + case "initial": + response.processAsync(); + response.write("do"); + var state = { + QueryInterface: ChromeUtils.generateQI([]), + end() { + response.write("ne"); + response.finish(); + }, + }; + state.wrappedJSObject = state; + setObjectState("object-state-test", state); + getObjectState("object-state-test", function (obj) { + if (obj !== state) { + response.write("FAIL bad state save"); + response.finish(); + } + }); + break; + + case "intermediate": + response.write("intermediate"); + break; + + case "trigger": + response.write("trigger"); + getObjectState("object-state-test", function (obj) { + obj.wrappedJSObject.end(); + setObjectState("object-state-test", null); + }); + break; + + default: + response.setStatusLine(request.httpVersion, 500, "Unexpected State"); + response.write("Bad state: " + params.state); + break; + } +} diff --git a/netwerk/test/httpserver/test/data/sjs/qi.sjs b/netwerk/test/httpserver/test/data/sjs/qi.sjs new file mode 100644 index 0000000000..ee0fc74a0f --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/qi.sjs @@ -0,0 +1,45 @@ +function handleRequest(request, response) { + var exstr, qid; + + response.setStatusLine(request.httpVersion, 500, "FAIL"); + + var passed = false; + try { + qid = request.QueryInterface(Ci.nsIHttpRequest); + passed = qid === request; + } catch (e) { + // eslint-disable-next-line no-control-regex + exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0]; + response.setStatusLine( + request.httpVersion, + 500, + "request doesn't QI: " + exstr + ); + return; + } + if (!passed) { + response.setStatusLine(request.httpVersion, 500, "request QI'd wrongly?"); + return; + } + + passed = false; + try { + qid = response.QueryInterface(Ci.nsIHttpResponse); + passed = qid === response; + } catch (e) { + // eslint-disable-next-line no-control-regex + exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0]; + response.setStatusLine( + request.httpVersion, + 500, + "response doesn't QI: " + exstr + ); + return; + } + if (!passed) { + response.setStatusLine(request.httpVersion, 500, "response QI'd wrongly?"); + return; + } + + response.setStatusLine(request.httpVersion, 200, "SJS QI Tests Passed"); +} diff --git a/netwerk/test/httpserver/test/data/sjs/range-checker.sjs b/netwerk/test/httpserver/test/data/sjs/range-checker.sjs new file mode 100644 index 0000000000..4bc447f739 --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/range-checker.sjs @@ -0,0 +1 @@ +function handleRequest(request, response) {} diff --git a/netwerk/test/httpserver/test/data/sjs/sjs b/netwerk/test/httpserver/test/data/sjs/sjs new file mode 100644 index 0000000000..374ca41674 --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) +{ + response.write("FAIL"); +} diff --git a/netwerk/test/httpserver/test/data/sjs/state1.sjs b/netwerk/test/httpserver/test/data/sjs/state1.sjs new file mode 100644 index 0000000000..1a2540eca1 --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/state1.sjs @@ -0,0 +1,38 @@ +function parseQueryString(str) { + var paramArray = str.split("&"); + var regex = /^([^=]+)=(.*)$/; + var params = {}; + for (var i = 0, sz = paramArray.length; i < sz; i++) { + var match = regex.exec(paramArray[i]); + if (!match) { + throw new Error("Bad parameter in queryString! '" + paramArray[i] + "'"); + } + params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); + } + + return params; +} + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + var params = parseQueryString(request.queryString); + + var oldShared = getSharedState("shared-value"); + response.setHeader("X-Old-Shared-Value", oldShared, false); + + var newShared = params.newShared; + if (newShared !== undefined) { + setSharedState("shared-value", newShared); + response.setHeader("X-New-Shared-Value", newShared, false); + } + + var oldPrivate = getState("private-value"); + response.setHeader("X-Old-Private-Value", oldPrivate, false); + + var newPrivate = params.newPrivate; + if (newPrivate !== undefined) { + setState("private-value", newPrivate); + response.setHeader("X-New-Private-Value", newPrivate, false); + } +} diff --git a/netwerk/test/httpserver/test/data/sjs/state2.sjs b/netwerk/test/httpserver/test/data/sjs/state2.sjs new file mode 100644 index 0000000000..1a2540eca1 --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/state2.sjs @@ -0,0 +1,38 @@ +function parseQueryString(str) { + var paramArray = str.split("&"); + var regex = /^([^=]+)=(.*)$/; + var params = {}; + for (var i = 0, sz = paramArray.length; i < sz; i++) { + var match = regex.exec(paramArray[i]); + if (!match) { + throw new Error("Bad parameter in queryString! '" + paramArray[i] + "'"); + } + params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); + } + + return params; +} + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + var params = parseQueryString(request.queryString); + + var oldShared = getSharedState("shared-value"); + response.setHeader("X-Old-Shared-Value", oldShared, false); + + var newShared = params.newShared; + if (newShared !== undefined) { + setSharedState("shared-value", newShared); + response.setHeader("X-New-Shared-Value", newShared, false); + } + + var oldPrivate = getState("private-value"); + response.setHeader("X-Old-Private-Value", oldPrivate, false); + + var newPrivate = params.newPrivate; + if (newPrivate !== undefined) { + setState("private-value", newPrivate); + response.setHeader("X-New-Private-Value", newPrivate, false); + } +} diff --git a/netwerk/test/httpserver/test/data/sjs/thrower.sjs b/netwerk/test/httpserver/test/data/sjs/thrower.sjs new file mode 100644 index 0000000000..b34de70e30 --- /dev/null +++ b/netwerk/test/httpserver/test/data/sjs/thrower.sjs @@ -0,0 +1,6 @@ +function handleRequest(request, response) { + if (request.queryString == "throw") { + undefined[5]; + } + response.setHeader("X-Test-Status", "PASS", false); +} diff --git a/netwerk/test/httpserver/test/head_utils.js b/netwerk/test/httpserver/test/head_utils.js new file mode 100644 index 0000000000..76e7ed6fd3 --- /dev/null +++ b/netwerk/test/httpserver/test/head_utils.js @@ -0,0 +1,578 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ +/* global __LOCATION__ */ +/* import-globals-from ../httpd.js */ + +var _HTTPD_JS_PATH = __LOCATION__.parent; +_HTTPD_JS_PATH.append("httpd.js"); +load(_HTTPD_JS_PATH.path); + +// if these tests fail, we'll want the debug output +var linDEBUG = true; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +/** + * Constructs a new nsHttpServer instance. This function is intended to + * encapsulate construction of a server so that at some point in the future it + * is possible to run these tests (with at most slight modifications) against + * the server when used as an XPCOM component (not as an inline script). + */ +function createServer() { + return new nsHttpServer(); +} + +/** + * Creates a new HTTP channel. + * + * @param url + * the URL of the channel to create + */ +function makeChannel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +/** + * Make a binary input stream wrapper for the given stream. + * + * @param stream + * the nsIInputStream to wrap + */ +function makeBIS(stream) { + return new BinaryInputStream(stream); +} + +/** + * Returns the contents of the file as a string. + * + * @param file : nsIFile + * the file whose contents are to be read + * @returns string + * the contents of the file + */ +function fileContents(file) { + const PR_RDONLY = 0x01; + var fis = new FileInputStream( + file, + PR_RDONLY, + 0o444, + Ci.nsIFileInputStream.CLOSE_ON_EOF + ); + var sis = new ScriptableInputStream(fis); + var contents = sis.read(file.fileSize); + sis.close(); + return contents; +} + +/** + * Iterates over the lines, delimited by CRLF, in data, returning each line + * without the trailing line separator. + * + * @param data : string + * a string consisting of lines of data separated by CRLFs + * @returns Iterator + * an Iterator which returns each line from data in turn; note that this + * includes a final empty line if data ended with a CRLF + */ +function* LineIterator(data) { + var index = 0; + do { + index = data.indexOf("\r\n"); + if (index >= 0) { + yield data.substring(0, index); + } else { + yield data; + } + + data = data.substring(index + 2); + } while (index >= 0); +} + +/** + * Throws if iter does not contain exactly the CRLF-separated lines in the + * array expectedLines. + * + * @param iter : Iterator + * an Iterator which returns lines of text + * @param expectedLines : [string] + * an array of the expected lines of text + * @throws an error message if iter doesn't agree with expectedLines + */ +function expectLines(iter, expectedLines) { + var index = 0; + for (var line of iter) { + if (expectedLines.length == index) { + throw new Error( + `Error: got more than ${expectedLines.length} expected lines!` + ); + } + + var expected = expectedLines[index++]; + if (expected !== line) { + throw new Error(`Error on line ${index}! + actual: '${line}', + expect: '${expected}'`); + } + } + + if (expectedLines.length !== index) { + throw new Error( + `Expected more lines! Got ${index}, expected ${expectedLines.length}` + ); + } +} + +/** + * Spew a bunch of HTTP metadata from request into the body of response. + * + * @param request : nsIHttpRequest + * the request whose metadata should be output + * @param response : nsIHttpResponse + * the response to which the metadata is written + */ +function writeDetails(request, response) { + response.write("Method: " + request.method + "\r\n"); + response.write("Path: " + request.path + "\r\n"); + response.write("Query: " + request.queryString + "\r\n"); + response.write("Version: " + request.httpVersion + "\r\n"); + response.write("Scheme: " + request.scheme + "\r\n"); + response.write("Host: " + request.host + "\r\n"); + response.write("Port: " + request.port); +} + +/** + * Advances iter past all non-blank lines and a single blank line, after which + * point the body of the response will be returned next from the iterator. + * + * @param iter : Iterator + * an iterator over the CRLF-delimited lines in an HTTP response, currently + * just after the Request-Line + */ +function skipHeaders(iter) { + var line = iter.next().value; + while (line !== "") { + line = iter.next().value; + } +} + +/** + * Checks that the exception e (which may be an XPConnect-created exception + * object or a raw nsresult number) is the given nsresult. + * + * @param e : Exception or nsresult + * the actual exception + * @param code : nsresult + * the expected exception + */ +function isException(e, code) { + if (e !== code && e.result !== code) { + do_throw("unexpected error: " + e); + } +} + +/** + * Calls the given function at least the specified number of milliseconds later. + * The callback will not undershoot the given time, but it might overshoot -- + * don't expect precision! + * + * @param milliseconds : uint + * the number of milliseconds to delay + * @param callback : function() : void + * the function to call + */ +function callLater(msecs, callback) { + do_timeout(msecs, callback); +} + +/** ***************************************************** + * SIMPLE SUPPORT FOR LOADING/TESTING A SERIES OF URLS * + *******************************************************/ + +/** + * Create a completion callback which will stop the given server and end the + * test, assuming nothing else remains to be done at that point. + */ +function testComplete(srv) { + return function complete() { + do_test_pending(); + srv.stop(function quit() { + do_test_finished(); + }); + }; +} + +/** + * Represents a path to load from the tested HTTP server, along with actions to + * take before, during, and after loading the associated page. + * + * @param path + * the URL to load from the server + * @param initChannel + * a function which takes as a single parameter a channel created for path and + * initializes its state, or null if no additional initialization is needed + * @param onStartRequest + * called during onStartRequest for the load of the URL, with the same + * parameters; the request parameter has been QI'd to nsIHttpChannel and + * nsIHttpChannelInternal for convenience; may be null if nothing needs to be + * done + * @param onStopRequest + * called during onStopRequest for the channel, with the same parameters plus + * a trailing parameter containing an array of the bytes of data downloaded in + * the body of the channel response; the request parameter has been QI'd to + * nsIHttpChannel and nsIHttpChannelInternal for convenience; may be null if + * nothing needs to be done + */ +function Test(path, initChannel, onStartRequest, onStopRequest) { + function nil() {} + + this.path = path; + this.initChannel = initChannel || nil; + this.onStartRequest = onStartRequest || nil; + this.onStopRequest = onStopRequest || nil; +} + +/** + * Runs all the tests in testArray. + * + * @param testArray + * a non-empty array of Tests to run, in order + * @param done + * function to call when all tests have run (e.g. to shut down the server) + */ +function runHttpTests(testArray, done) { + /** Kicks off running the next test in the array. */ + function performNextTest() { + if (++testIndex == testArray.length) { + try { + done(); + } catch (e) { + do_report_unexpected_exception(e, "running test-completion callback"); + } + return; + } + + do_test_pending(); + + var test = testArray[testIndex]; + var ch = makeChannel(test.path); + try { + test.initChannel(ch); + } catch (e) { + try { + do_report_unexpected_exception( + e, + "testArray[" + testIndex + "].initChannel(ch)" + ); + } catch (x) { + /* swallow and let tests continue */ + } + } + + listener._channel = ch; + ch.asyncOpen(listener); + } + + /** Index of the test being run. */ + var testIndex = -1; + + /** Stream listener for the channels. */ + var listener = { + /** Current channel being observed by this. */ + _channel: null, + /** Array of bytes of data in body of response. */ + _data: [], + + onStartRequest(request) { + Assert.ok(request === this._channel); + var ch = request + .QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + this._data.length = 0; + try { + try { + testArray[testIndex].onStartRequest(ch); + } catch (e) { + do_report_unexpected_exception( + e, + "testArray[" + testIndex + "].onStartRequest" + ); + } + } catch (e) { + do_note_exception( + e, + "!!! swallowing onStartRequest exception so onStopRequest is " + + "called..." + ); + } + }, + onDataAvailable(request, inputStream, offset, count) { + var quantum = 262144; // just above half the argument-count limit + var bis = makeBIS(inputStream); + for (var start = 0; start < count; start += quantum) { + var newData = bis.readByteArray(Math.min(quantum, count - start)); + Array.prototype.push.apply(this._data, newData); + } + }, + onStopRequest(request, status) { + this._channel = null; + + var ch = request + .QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + // NB: The onStopRequest callback must run before performNextTest here, + // because the latter runs the next test's initChannel callback, and + // we want one test to be sequentially processed before the next + // one. + try { + testArray[testIndex].onStopRequest(ch, status, this._data); + } catch (e) { + do_report_unexpected_exception( + e, + "testArray[" + testIndex + "].onStartRequest" + ); + } finally { + try { + performNextTest(); + } finally { + do_test_finished(); + } + } + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + }; + + performNextTest(); +} + +/** ************************************** + * RAW REQUEST FORMAT TESTING FUNCTIONS * + ****************************************/ + +/** + * Sends a raw string of bytes to the given host and port and checks that the + * response is acceptable. + * + * @param host : string + * the host to which a connection should be made + * @param port : PRUint16 + * the port to use for the connection + * @param data : string or [string...] + * either: + * - the raw data to send, as a string of characters with codes in the + * range 0-255, or + * - an array of such strings whose concatenation forms the raw data + * @param responseCheck : function(string) : void + * a function which is provided with the data sent by the remote host which + * conducts whatever tests it wants on that data; useful for tweaking the test + * environment between tests + */ +function RawTest(host, port, data, responseCheck) { + if (0 > port || 65535 < port || port % 1 !== 0) { + throw new Error("bad port"); + } + if (!(data instanceof Array)) { + data = [data]; + } + if (data.length <= 0) { + throw new Error("bad data length"); + } + + if ( + !data.every(function (v) { + // eslint-disable-next-line no-control-regex + return /^[\x00-\xff]*$/.test(v); + }) + ) { + throw new Error("bad data contained non-byte-valued character"); + } + + this.host = host; + this.port = port; + this.data = data; + this.responseCheck = responseCheck; +} + +/** + * Runs all the tests in testArray, an array of RawTests. + * + * @param testArray : [RawTest] + * an array of RawTests to run, in order + * @param done + * function to call when all tests have run (e.g. to shut down the server) + * @param beforeTestCallback + * function to call before each test is run. Gets passed testIndex when called + */ +function runRawTests(testArray, done, beforeTestCallback) { + do_test_pending(); + + var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( + Ci.nsISocketTransportService + ); + + var currentThread = + Cc["@mozilla.org/thread-manager;1"].getService().currentThread; + + /** Kicks off running the next test in the array. */ + function performNextTest() { + if (++testIndex == testArray.length) { + do_test_finished(); + try { + done(); + } catch (e) { + do_report_unexpected_exception(e, "running test-completion callback"); + } + return; + } + + if (beforeTestCallback) { + try { + beforeTestCallback(testIndex); + } catch (e) { + /* We don't care if this call fails */ + } + } + + var rawTest = testArray[testIndex]; + + var transport = sts.createTransport( + [], + rawTest.host, + rawTest.port, + null, + null + ); + + var inStream = transport.openInputStream(0, 0, 0); + var outStream = transport.openOutputStream(0, 0, 0); + + // reset + dataIndex = 0; + received = ""; + + waitForMoreInput(inStream); + waitToWriteOutput(outStream); + } + + function waitForMoreInput(stream) { + reader.stream = stream; + stream = stream.QueryInterface(Ci.nsIAsyncInputStream); + stream.asyncWait(reader, 0, 0, currentThread); + } + + function waitToWriteOutput(stream) { + // Do the QueryInterface here, not earlier, because there is no + // guarantee that 'stream' passed in here been QIed to nsIAsyncOutputStream + // since the last GC. + stream = stream.QueryInterface(Ci.nsIAsyncOutputStream); + stream.asyncWait( + writer, + 0, + testArray[testIndex].data[dataIndex].length, + currentThread + ); + } + + /** Index of the test being run. */ + var testIndex = -1; + + /** + * Index of remaining data strings to be written to the socket in current + * test. + */ + var dataIndex = 0; + + /** Data received so far from the server. */ + var received = ""; + + /** Reads data from the socket. */ + var reader = { + onInputStreamReady(stream) { + Assert.ok(stream === this.stream); + try { + var bis = new BinaryInputStream(stream); + + var av = 0; + try { + av = bis.available(); + } catch (e) { + /* default to 0 */ + do_note_exception(e); + } + + if (av > 0) { + var quantum = 262144; + for (var start = 0; start < av; start += quantum) { + var bytes = bis.readByteArray(Math.min(quantum, av - start)); + received += String.fromCharCode.apply(null, bytes); + } + waitForMoreInput(stream); + return; + } + } catch (e) { + do_report_unexpected_exception(e); + } + + var rawTest = testArray[testIndex]; + try { + rawTest.responseCheck(received); + } catch (e) { + do_report_unexpected_exception(e); + } finally { + try { + stream.close(); + performNextTest(); + } catch (e) { + do_report_unexpected_exception(e); + } + } + }, + }; + + /** Writes data to the socket. */ + var writer = { + onOutputStreamReady(stream) { + var str = testArray[testIndex].data[dataIndex]; + + var written = 0; + try { + written = stream.write(str, str.length); + if (written == str.length) { + dataIndex++; + } else { + testArray[testIndex].data[dataIndex] = str.substring(written); + } + } catch (e) { + do_note_exception(e); + /* stream could have been closed, just ignore */ + } + + try { + // Keep writing data while we can write and + // until there's no more data to read + if (written > 0 && dataIndex < testArray[testIndex].data.length) { + waitToWriteOutput(stream); + } else { + stream.close(); + } + } catch (e) { + do_report_unexpected_exception(e); + } + }, + }; + + performNextTest(); +} diff --git a/netwerk/test/httpserver/test/test_async_response_sending.js b/netwerk/test/httpserver/test/test_async_response_sending.js new file mode 100644 index 0000000000..abc9741c1e --- /dev/null +++ b/netwerk/test/httpserver/test/test_async_response_sending.js @@ -0,0 +1,1658 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* + * Ensures that data a request handler writes out in response is sent only as + * quickly as the client can receive it, without racing ahead and being forced + * to block while writing that data. + * + * NB: These tests are extremely tied to the current implementation, in terms of + * when and how stream-ready notifications occur, the amount of data which will + * be read or written at each notification, and so on. If the implementation + * changes in any way with respect to stream copying, this test will probably + * have to change a little at the edges as well. + */ + +gThreadManager = Cc["@mozilla.org/thread-manager;1"].createInstance(); + +function run_test() { + do_test_pending(); + tests.push(function testsComplete(_) { + dumpn( + // eslint-disable-next-line no-useless-concat + "******************\n" + "* TESTS COMPLETE *\n" + "******************" + ); + do_test_finished(); + }); + + runNextTest(); +} + +function runNextTest() { + testIndex++; + dumpn("*** runNextTest(), testIndex: " + testIndex); + + try { + var test = tests[testIndex]; + test(runNextTest); + } catch (e) { + var msg = "exception running test " + testIndex + ": " + e; + if (e && "stack" in e) { + msg += "\nstack follows:\n" + e.stack; + } + do_throw(msg); + } +} + +/** *********** + * TEST DATA * + *************/ + +const NOTHING = []; + +const FIRST_SEGMENT = [1, 2, 3, 4]; +const SECOND_SEGMENT = [5, 6, 7, 8]; +const THIRD_SEGMENT = [9, 10, 11, 12]; + +const SEGMENT = FIRST_SEGMENT; +const TWO_SEGMENTS = [1, 2, 3, 4, 5, 6, 7, 8]; +const THREE_SEGMENTS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + +const SEGMENT_AND_HALF = [1, 2, 3, 4, 5, 6]; + +const QUARTER_SEGMENT = [1]; +const HALF_SEGMENT = [1, 2]; +const SECOND_HALF_SEGMENT = [3, 4]; +const EXTRA_HALF_SEGMENT = [5, 6]; +const MIDDLE_HALF_SEGMENT = [2, 3]; +const LAST_QUARTER_SEGMENT = [4]; +const HALF_THIRD_SEGMENT = [9, 10]; +const LATTER_HALF_THIRD_SEGMENT = [11, 12]; + +const TWO_HALF_SEGMENTS = [1, 2, 1, 2]; + +/** ******* + * TESTS * + *********/ + +var tests = [ + sourceClosedWithoutWrite, + writeOneSegmentThenClose, + simpleWriteThenRead, + writeLittleBeforeReading, + writeMultipleSegmentsThenRead, + writeLotsBeforeReading, + writeLotsBeforeReading2, + writeThenReadPartial, + manyPartialWrites, + partialRead, + partialWrite, + sinkClosedImmediately, + sinkClosedWithReadableData, + sinkClosedAfterWrite, + sourceAndSinkClosed, + sinkAndSourceClosed, + sourceAndSinkClosedWithPendingData, + sinkAndSourceClosedWithPendingData, +]; +var testIndex = -1; + +function sourceClosedWithoutWrite(next) { + var t = new CopyTest("sourceClosedWithoutWrite", next); + + t.closeSource(Cr.NS_OK); + t.expect(Cr.NS_OK, [NOTHING]); +} + +function writeOneSegmentThenClose(next) { + var t = new CopyTest("writeLittleBeforeReading", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.closeSource(Cr.NS_OK); + t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]); + t.expect(Cr.NS_OK, [SEGMENT]); +} + +function simpleWriteThenRead(next) { + var t = new CopyTest("simpleWriteThenRead", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]); + t.closeSource(Cr.NS_OK); + t.expect(Cr.NS_OK, [SEGMENT]); +} + +function writeLittleBeforeReading(next) { + var t = new CopyTest("writeLittleBeforeReading", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.closeSource(Cr.NS_OK); + t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]); + t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]); + t.expect(Cr.NS_OK, [SEGMENT, SEGMENT]); +} + +function writeMultipleSegmentsThenRead(next) { + var t = new CopyTest("writeMultipleSegmentsThenRead", next); + + t.addToSource(TWO_SEGMENTS); + t.makeSourceReadable(TWO_SEGMENTS.length); + t.makeSinkWritableAndWaitFor(TWO_SEGMENTS.length, [ + FIRST_SEGMENT, + SECOND_SEGMENT, + ]); + t.closeSource(Cr.NS_OK); + t.expect(Cr.NS_OK, [TWO_SEGMENTS]); +} + +function writeLotsBeforeReading(next) { + var t = new CopyTest("writeLotsBeforeReading", next); + + t.addToSource(TWO_SEGMENTS); + t.makeSourceReadable(TWO_SEGMENTS.length); + t.makeSinkWritableAndWaitFor(FIRST_SEGMENT.length, [FIRST_SEGMENT]); + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.makeSinkWritableAndWaitFor(SECOND_SEGMENT.length, [SECOND_SEGMENT]); + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.closeSource(Cr.NS_OK); + t.makeSinkWritableAndWaitFor(2 * SEGMENT.length, [SEGMENT, SEGMENT]); + t.expect(Cr.NS_OK, [TWO_SEGMENTS, SEGMENT, SEGMENT]); +} + +function writeLotsBeforeReading2(next) { + var t = new CopyTest("writeLotsBeforeReading", next); + + t.addToSource(THREE_SEGMENTS); + t.makeSourceReadable(THREE_SEGMENTS.length); + t.makeSinkWritableAndWaitFor(FIRST_SEGMENT.length, [FIRST_SEGMENT]); + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.makeSinkWritableAndWaitFor(SECOND_SEGMENT.length, [SECOND_SEGMENT]); + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.makeSinkWritableAndWaitFor(THIRD_SEGMENT.length, [THIRD_SEGMENT]); + t.closeSource(Cr.NS_OK); + t.makeSinkWritableAndWaitFor(2 * SEGMENT.length, [SEGMENT, SEGMENT]); + t.expect(Cr.NS_OK, [THREE_SEGMENTS, SEGMENT, SEGMENT]); +} + +function writeThenReadPartial(next) { + var t = new CopyTest("writeThenReadPartial", next); + + t.addToSource(SEGMENT_AND_HALF); + t.makeSourceReadable(SEGMENT_AND_HALF.length); + t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]); + t.closeSource(Cr.NS_OK); + t.makeSinkWritableAndWaitFor(EXTRA_HALF_SEGMENT.length, [EXTRA_HALF_SEGMENT]); + t.expect(Cr.NS_OK, [SEGMENT_AND_HALF]); +} + +function manyPartialWrites(next) { + var t = new CopyTest("manyPartialWrites", next); + + t.addToSource(HALF_SEGMENT); + t.makeSourceReadable(HALF_SEGMENT.length); + + t.addToSource(HALF_SEGMENT); + t.makeSourceReadable(HALF_SEGMENT.length); + t.makeSinkWritableAndWaitFor(2 * HALF_SEGMENT.length, [TWO_HALF_SEGMENTS]); + t.closeSource(Cr.NS_OK); + t.expect(Cr.NS_OK, [TWO_HALF_SEGMENTS]); +} + +function partialRead(next) { + var t = new CopyTest("partialRead", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.addToSource(HALF_SEGMENT); + t.makeSourceReadable(HALF_SEGMENT.length); + t.makeSinkWritableAndWaitFor(SEGMENT.length, [SEGMENT]); + t.closeSourceAndWaitFor(Cr.NS_OK, HALF_SEGMENT.length, [HALF_SEGMENT]); + t.expect(Cr.NS_OK, [SEGMENT, HALF_SEGMENT]); +} + +function partialWrite(next) { + var t = new CopyTest("partialWrite", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.makeSinkWritableByIncrementsAndWaitFor(SEGMENT.length, [ + QUARTER_SEGMENT, + MIDDLE_HALF_SEGMENT, + LAST_QUARTER_SEGMENT, + ]); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.makeSinkWritableByIncrementsAndWaitFor(SEGMENT.length, [ + HALF_SEGMENT, + SECOND_HALF_SEGMENT, + ]); + + t.addToSource(THREE_SEGMENTS); + t.makeSourceReadable(THREE_SEGMENTS.length); + t.makeSinkWritableByIncrementsAndWaitFor(THREE_SEGMENTS.length, [ + HALF_SEGMENT, + SECOND_HALF_SEGMENT, + SECOND_SEGMENT, + HALF_THIRD_SEGMENT, + LATTER_HALF_THIRD_SEGMENT, + ]); + + t.closeSource(Cr.NS_OK); + t.expect(Cr.NS_OK, [SEGMENT, SEGMENT, THREE_SEGMENTS]); +} + +function sinkClosedImmediately(next) { + var t = new CopyTest("sinkClosedImmediately", next); + + t.closeSink(Cr.NS_OK); + t.expect(Cr.NS_ERROR_UNEXPECTED, [NOTHING]); +} + +function sinkClosedWithReadableData(next) { + var t = new CopyTest("sinkClosedWithReadableData", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + t.closeSink(Cr.NS_OK); + t.expect(Cr.NS_ERROR_UNEXPECTED, [NOTHING]); +} + +function sinkClosedAfterWrite(next) { + var t = new CopyTest("sinkClosedAfterWrite", next); + + t.addToSource(TWO_SEGMENTS); + t.makeSourceReadable(TWO_SEGMENTS.length); + t.makeSinkWritableAndWaitFor(FIRST_SEGMENT.length, [FIRST_SEGMENT]); + t.closeSink(Cr.NS_OK); + t.expect(Cr.NS_ERROR_UNEXPECTED, [FIRST_SEGMENT]); +} + +function sourceAndSinkClosed(next) { + var t = new CopyTest("sourceAndSinkClosed", next); + + t.closeSourceThenSink(Cr.NS_OK, Cr.NS_OK); + t.expect(Cr.NS_OK, []); +} + +function sinkAndSourceClosed(next) { + var t = new CopyTest("sinkAndSourceClosed", next); + + t.closeSinkThenSource(Cr.NS_OK, Cr.NS_OK); + + // sink notify received first, hence error + t.expect(Cr.NS_ERROR_UNEXPECTED, []); +} + +function sourceAndSinkClosedWithPendingData(next) { + var t = new CopyTest("sourceAndSinkClosedWithPendingData", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + + t.closeSourceThenSink(Cr.NS_OK, Cr.NS_OK); + + // not all data from source copied, so error + t.expect(Cr.NS_ERROR_UNEXPECTED, []); +} + +function sinkAndSourceClosedWithPendingData(next) { + var t = new CopyTest("sinkAndSourceClosedWithPendingData", next); + + t.addToSource(SEGMENT); + t.makeSourceReadable(SEGMENT.length); + + t.closeSinkThenSource(Cr.NS_OK, Cr.NS_OK); + + // not all data from source copied, plus sink notify received first, so error + t.expect(Cr.NS_ERROR_UNEXPECTED, []); +} + +/** *********** + * UTILITIES * + *************/ + +/** Returns the sum of the elements in arr. */ +function sum(arr) { + var s = 0; + for (var i = 0, sz = arr.length; i < sz; i++) { + s += arr[i]; + } + return s; +} + +/** + * Returns a constructor for an input or output stream callback that will wrap + * the one provided to it as an argument. + * + * @param wrapperCallback : (nsIInputStreamCallback | nsIOutputStreamCallback) : void + * the original callback object (not a function!) being wrapped + * @param name : string + * either "onInputStreamReady" if we're wrapping an input stream callback or + * "onOutputStreamReady" if we're wrapping an output stream callback + * @returns function(nsIInputStreamCallback | nsIOutputStreamCallback) : (nsIInputStreamCallback | nsIOutputStreamCallback) + * a constructor function which constructs a callback object (not function!) + * which, when called, first calls the original callback provided to it and + * then calls wrapperCallback + */ +function createStreamReadyInterceptor(wrapperCallback, name) { + return function StreamReadyInterceptor(callback) { + this.wrappedCallback = callback; + this[name] = function streamReadyInterceptor(stream) { + dumpn("*** StreamReadyInterceptor." + name); + + try { + dumpn("*** calling original " + name + "..."); + callback[name](stream); + } catch (e) { + dumpn("!!! error running inner callback: " + e); + throw e; + } finally { + dumpn("*** calling wrapper " + name + "..."); + wrapperCallback[name](stream); + } + }; + }; +} + +/** + * Print out a banner with the given message, uppercased, for debugging + * purposes. + */ +function note(m) { + m = m.toUpperCase(); + var asterisks = Array(m.length + 1 + 4).join("*"); + dumpn(asterisks + "\n* " + m + " *\n" + asterisks); +} + +/** ********* + * MOCKERY * + ***********/ + +/* + * Blatantly violate abstractions in the name of testability. THIS IS NOT + * PUBLIC API! If you use any of these I will knowingly break your code by + * changing the names of variables and properties. + */ +// These are used in head.js. +/* exported BinaryInputStream, BinaryOutputStream */ +var BinaryInputStream = function BIS(stream) { + return stream; +}; +var BinaryOutputStream = function BOS(stream) { + return stream; +}; +Response.SEGMENT_SIZE = SEGMENT.length; + +/** + * Roughly mocks an nsIPipe, presenting non-blocking input and output streams + * that appear to also be binary streams and whose readability and writability + * amounts are configurable. Only the methods used in this test have been + * implemented -- these aren't exact mocks (can't be, actually, because input + * streams have unscriptable methods). + * + * @param name : string + * a name for this pipe, used in debugging output + */ +function CustomPipe(name) { + var self = this; + + /** Data read from input that's buffered until it can be written to output. */ + this._data = []; + + /** + * The status of this pipe, which is to say the error result the ends of this + * pipe will return when attempts are made to use them. This value is always + * an error result when copying has finished, because success codes are + * converted to NS_BASE_STREAM_CLOSED. + */ + this._status = Cr.NS_OK; + + /** The input end of this pipe. */ + var input = (this.inputStream = { + /** A name for this stream, used in debugging output. */ + name: name + " input", + + /** + * The number of bytes of data available to be read from this pipe, or + * Infinity if any amount of data in this pipe is made readable as soon as + * it is written to the pipe output. + */ + _readable: 0, + + /** + * Data regarding a pending stream-ready callback on this, or null if no + * callback is currently waiting to be called. + */ + _waiter: null, + + /** + * The event currently dispatched to make a stream-ready callback, if any + * such callback is currently ready to be made and not already in + * progress, or null when no callback is waiting to happen. + */ + _event: null, + + /** + * A stream-ready constructor to wrap an existing callback to intercept + * stream-ready notifications, or null if notifications shouldn't be + * wrapped at all. + */ + _streamReadyInterceptCreator: null, + + /** + * Registers a stream-ready wrapper creator function so that a + * stream-ready callback made in the future can be wrapped. + */ + interceptStreamReadyCallbacks(streamReadyInterceptCreator) { + dumpn("*** [" + this.name + "].interceptStreamReadyCallbacks"); + + Assert.ok( + this._streamReadyInterceptCreator === null, + "intercepting twice" + ); + this._streamReadyInterceptCreator = streamReadyInterceptCreator; + if (this._waiter) { + this._waiter.callback = new streamReadyInterceptCreator( + this._waiter.callback + ); + } + }, + + /** + * Removes a previously-registered stream-ready wrapper creator function, + * also clearing any current wrapping. + */ + removeStreamReadyInterceptor() { + dumpn("*** [" + this.name + "].removeStreamReadyInterceptor()"); + + Assert.ok( + this._streamReadyInterceptCreator !== null, + "removing interceptor when none present?" + ); + this._streamReadyInterceptCreator = null; + if (this._waiter) { + this._waiter.callback = this._waiter.callback.wrappedCallback; + } + }, + + // + // see nsIAsyncInputStream.asyncWait + // + asyncWait: function asyncWait(callback, flags, requestedCount, target) { + dumpn("*** [" + this.name + "].asyncWait"); + + Assert.ok(callback && typeof callback !== "function"); + + var closureOnly = + (flags & Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY) !== 0; + + Assert.ok( + this._waiter === null || (this._waiter.closureOnly && !closureOnly), + "asyncWait already called with a non-closure-only " + + "callback? unexpected!" + ); + + this._waiter = { + callback: this._streamReadyInterceptCreator + ? new this._streamReadyInterceptCreator(callback) + : callback, + closureOnly, + requestedCount, + eventTarget: target, + }; + + if ( + !Components.isSuccessCode(self._status) || + (!closureOnly && + this._readable >= requestedCount && + self._data.length >= requestedCount) + ) { + this._notify(); + } + }, + + // + // see nsIAsyncInputStream.closeWithStatus + // + closeWithStatus: function closeWithStatus(status) { + // eslint-disable-next-line no-useless-concat + dumpn("*** [" + this.name + "].closeWithStatus" + "(" + status + ")"); + + if (!Components.isSuccessCode(self._status)) { + dumpn( + "*** ignoring second closure of [input " + + this.name + + "] " + + "(status " + + self._status + + ")" + ); + return; + } + + if (Components.isSuccessCode(status)) { + status = Cr.NS_BASE_STREAM_CLOSED; + } + + self._status = status; + + if (this._waiter) { + this._notify(); + } + if (output._waiter) { + output._notify(); + } + }, + + // + // see nsIBinaryInputStream.readByteArray + // + readByteArray: function readByteArray(count) { + dumpn("*** [" + this.name + "].readByteArray(" + count + ")"); + + if (self._data.length === 0) { + throw Components.isSuccessCode(self._status) + ? Cr.NS_BASE_STREAM_WOULD_BLOCK + : self._status; + } + + Assert.ok( + this._readable <= self._data.length || this._readable === Infinity, + "consistency check" + ); + + if (this._readable < count || self._data.length < count) { + throw Components.Exception("", Cr.NS_BASE_STREAM_WOULD_BLOCK); + } + this._readable -= count; + return self._data.splice(0, count); + }, + + /** + * Makes the given number of additional bytes of data previously written + * to the pipe's output stream available for reading, triggering future + * notifications when required. + * + * @param count : uint + * the number of bytes of additional data to make available; must not be + * greater than the number of bytes already buffered but not made + * available by previous makeReadable calls + */ + makeReadable: function makeReadable(count) { + dumpn("*** [" + this.name + "].makeReadable(" + count + ")"); + + Assert.ok(Components.isSuccessCode(self._status), "errant call"); + Assert.ok( + this._readable + count <= self._data.length || + this._readable === Infinity, + "increasing readable beyond written amount" + ); + + this._readable += count; + + dumpn("readable: " + this._readable + ", data: " + self._data); + + var waiter = this._waiter; + if (waiter !== null) { + if (waiter.requestedCount <= this._readable && !waiter.closureOnly) { + this._notify(); + } + } + }, + + /** + * Disables the readability limit on this stream, meaning that as soon as + * *any* amount of data is written to output it becomes available from + * this stream and a stream-ready event is dispatched (if any stream-ready + * callback is currently set). + */ + disableReadabilityLimit: function disableReadabilityLimit() { + dumpn("*** [" + this.name + "].disableReadabilityLimit()"); + + this._readable = Infinity; + }, + + // + // see nsIInputStream.available + // + available: function available() { + dumpn("*** [" + this.name + "].available()"); + + if (self._data.length === 0 && !Components.isSuccessCode(self._status)) { + throw self._status; + } + + return Math.min(this._readable, self._data.length); + }, + + /** + * Dispatches a pending stream-ready event ahead of schedule, rather than + * waiting for it to be dispatched in response to normal writes. This is + * useful when writing to the output has completed, and we need to have + * read all data written to this stream. If the output isn't closed and + * the reading of data from this races ahead of the last write to output, + * we need a notification to know when everything that's been written has + * been read. This ordinarily might be supplied by closing output, but + * in some cases it's not desirable to close output, so this supplies an + * alternative method to get notified when the last write has occurred. + */ + maybeNotifyFinally: function maybeNotifyFinally() { + dumpn("*** [" + this.name + "].maybeNotifyFinally()"); + + Assert.ok(this._waiter !== null, "must be waiting now"); + + if (self._data.length) { + dumpn( + "*** data still pending, normal notifications will signal " + + "completion" + ); + return; + } + + // No data waiting to be written, so notify. We could just close the + // stream, but that's less faithful to the server's behavior (it doesn't + // close the stream, and we're pretending to impersonate the server as + // much as we can here), so instead we're going to notify when no data + // can be read. The CopyTest has already been flagged as complete, so + // the stream listener will detect that this is a wrap-it-up notify and + // invoke the next test. + this._notify(); + }, + + /** + * Dispatches an event to call a previously-registered stream-ready + * callback. + */ + _notify: function _notify() { + dumpn("*** [" + this.name + "]._notify()"); + + var waiter = this._waiter; + Assert.ok(waiter !== null, "no waiter?"); + + if (this._event === null) { + var event = (this._event = { + run: function run() { + input._waiter = null; + input._event = null; + try { + Assert.ok( + !Components.isSuccessCode(self._status) || + input._readable >= waiter.requestedCount + ); + waiter.callback.onInputStreamReady(input); + } catch (e) { + do_throw("error calling onInputStreamReady: " + e); + } + }, + }); + waiter.eventTarget.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + } + }, + }); + + /** The output end of this pipe. */ + var output = (this.outputStream = { + /** A name for this stream, used in debugging output. */ + name: name + " output", + + /** + * The number of bytes of data which may be written to this pipe without + * blocking. + */ + _writable: 0, + + /** + * The increments in which pending data should be written, rather than + * simply defaulting to the amount requested (which, given that + * input.asyncWait precisely respects the requestedCount argument, will + * ordinarily always be writable in that amount), as an array whose + * elements from start to finish are the number of bytes to write each + * time write() or writeByteArray() is subsequently called. The sum of + * the values in this array, if this array is not empty, is always equal + * to this._writable. + */ + _writableAmounts: [], + + /** + * Data regarding a pending stream-ready callback on this, or null if no + * callback is currently waiting to be called. + */ + _waiter: null, + + /** + * The event currently dispatched to make a stream-ready callback, if any + * such callback is currently ready to be made and not already in + * progress, or null when no callback is waiting to happen. + */ + _event: null, + + /** + * A stream-ready constructor to wrap an existing callback to intercept + * stream-ready notifications, or null if notifications shouldn't be + * wrapped at all. + */ + _streamReadyInterceptCreator: null, + + /** + * Registers a stream-ready wrapper creator function so that a + * stream-ready callback made in the future can be wrapped. + */ + interceptStreamReadyCallbacks(streamReadyInterceptCreator) { + dumpn("*** [" + this.name + "].interceptStreamReadyCallbacks"); + + Assert.ok( + this._streamReadyInterceptCreator !== null, + "intercepting onOutputStreamReady twice" + ); + this._streamReadyInterceptCreator = streamReadyInterceptCreator; + if (this._waiter) { + this._waiter.callback = new streamReadyInterceptCreator( + this._waiter.callback + ); + } + }, + + /** + * Removes a previously-registered stream-ready wrapper creator function, + * also clearing any current wrapping. + */ + removeStreamReadyInterceptor() { + dumpn("*** [" + this.name + "].removeStreamReadyInterceptor()"); + + Assert.ok( + this._streamReadyInterceptCreator !== null, + "removing interceptor when none present?" + ); + this._streamReadyInterceptCreator = null; + if (this._waiter) { + this._waiter.callback = this._waiter.callback.wrappedCallback; + } + }, + + // + // see nsIAsyncOutputStream.asyncWait + // + asyncWait: function asyncWait(callback, flags, requestedCount, target) { + dumpn("*** [" + this.name + "].asyncWait"); + + Assert.ok(callback && typeof callback !== "function"); + + var closureOnly = + (flags & Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY) !== 0; + + Assert.ok( + this._waiter === null || (this._waiter.closureOnly && !closureOnly), + "asyncWait already called with a non-closure-only " + + "callback? unexpected!" + ); + + this._waiter = { + callback: this._streamReadyInterceptCreator + ? new this._streamReadyInterceptCreator(callback) + : callback, + closureOnly, + requestedCount, + eventTarget: target, + toString: function toString() { + return ( + "waiter(" + + (closureOnly ? "closure only, " : "") + + "requestedCount: " + + requestedCount + + ", target: " + + target + + ")" + ); + }, + }; + + if ( + (!closureOnly && this._writable >= requestedCount) || + !Components.isSuccessCode(this.status) + ) { + this._notify(); + } + }, + + // + // see nsIAsyncOutputStream.closeWithStatus + // + closeWithStatus: function closeWithStatus(status) { + dumpn("*** [" + this.name + "].closeWithStatus(" + status + ")"); + + if (!Components.isSuccessCode(self._status)) { + dumpn( + "*** ignoring redundant closure of [input " + + this.name + + "] " + + "because it's already closed (status " + + self._status + + ")" + ); + return; + } + + if (Components.isSuccessCode(status)) { + status = Cr.NS_BASE_STREAM_CLOSED; + } + + self._status = status; + + if (input._waiter) { + input._notify(); + } + if (this._waiter) { + this._notify(); + } + }, + + // + // see nsIBinaryOutputStream.writeByteArray + // + writeByteArray: function writeByteArray(bytes) { + dumpn(`*** [${this.name}].writeByteArray([${bytes}])`); + + if (!Components.isSuccessCode(self._status)) { + throw self._status; + } + + Assert.equal( + this._writableAmounts.length, + 0, + "writeByteArray can't support specified-length writes" + ); + + if (this._writable < bytes.length) { + throw Components.Exception("", Cr.NS_BASE_STREAM_WOULD_BLOCK); + } + + self._data.push.apply(self._data, bytes); + this._writable -= bytes.length; + + if ( + input._readable === Infinity && + input._waiter && + !input._waiter.closureOnly + ) { + input._notify(); + } + }, + + // + // see nsIOutputStream.write + // + write: function write(str, length) { + dumpn("*** [" + this.name + "].write"); + + Assert.equal(str.length, length, "sanity"); + if (!Components.isSuccessCode(self._status)) { + throw self._status; + } + if (this._writable === 0) { + throw Components.Exception("", Cr.NS_BASE_STREAM_WOULD_BLOCK); + } + + var actualWritten; + if (this._writableAmounts.length === 0) { + actualWritten = Math.min(this._writable, length); + } else { + Assert.ok( + this._writable >= this._writableAmounts[0], + "writable amounts value greater than writable data?" + ); + Assert.equal( + this._writable, + sum(this._writableAmounts), + "total writable amount not equal to sum of writable increments" + ); + actualWritten = this._writableAmounts.shift(); + } + + var bytes = str + .substring(0, actualWritten) + .split("") + .map(function (v) { + return v.charCodeAt(0); + }); + + self._data.push.apply(self._data, bytes); + this._writable -= actualWritten; + + if ( + input._readable === Infinity && + input._waiter && + !input._waiter.closureOnly + ) { + input._notify(); + } + + return actualWritten; + }, + + /** + * Increase the amount of data that can be written without blocking by the + * given number of bytes, triggering future notifications when required. + * + * @param count : uint + * the number of bytes of additional data to make writable + */ + makeWritable: function makeWritable(count) { + dumpn("*** [" + this.name + "].makeWritable(" + count + ")"); + + Assert.ok(Components.isSuccessCode(self._status)); + + this._writable += count; + + var waiter = this._waiter; + if ( + waiter && + !waiter.closureOnly && + waiter.requestedCount <= this._writable + ) { + this._notify(); + } + }, + + /** + * Increase the amount of data that can be written without blocking, but + * do so by specifying a number of bytes that will be written each time + * a write occurs, even as asyncWait notifications are initially triggered + * as usual. Thus, rather than writes eagerly writing everything possible + * at each step, attempts to write out data by segment devolve into a + * partial segment write, then another, and so on until the amount of data + * specified as permitted to be written, has been written. + * + * Note that the writeByteArray method is incompatible with the previous + * calling of this method, in that, until all increments provided to this + * method have been consumed, writeByteArray cannot be called. Once all + * increments have been consumed, writeByteArray may again be called. + * + * @param increments : [uint] + * an array whose elements are positive numbers of bytes to permit to be + * written each time write() is subsequently called on this, ignoring + * the total amount of writable space specified by the sum of all + * increments + */ + makeWritableByIncrements: function makeWritableByIncrements(increments) { + dumpn( + "*** [" + + this.name + + "].makeWritableByIncrements" + + "([" + + increments.join(", ") + + "])" + ); + + Assert.greater(increments.length, 0, "bad increments"); + Assert.ok( + increments.every(function (v) { + return v > 0; + }), + "zero increment?" + ); + + Assert.ok(Components.isSuccessCode(self._status)); + + this._writable += sum(increments); + this._writableAmounts = increments; + + var waiter = this._waiter; + if ( + waiter && + !waiter.closureOnly && + waiter.requestedCount <= this._writable + ) { + this._notify(); + } + }, + + /** + * Dispatches an event to call a previously-registered stream-ready + * callback. + */ + _notify: function _notify() { + dumpn("*** [" + this.name + "]._notify()"); + + var waiter = this._waiter; + Assert.ok(waiter !== null, "no waiter?"); + + if (this._event === null) { + var event = (this._event = { + run: function run() { + output._waiter = null; + output._event = null; + + try { + waiter.callback.onOutputStreamReady(output); + } catch (e) { + do_throw("error calling onOutputStreamReady: " + e); + } + }, + }); + waiter.eventTarget.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + } + }, + }); +} + +/** + * Represents a sequence of interactions to perform with a copier, in a given + * order and at the desired time intervals. + * + * @param name : string + * test name, used in debugging output + */ +function CopyTest(name, next) { + /** Name used in debugging output. */ + this.name = name; + + /** A function called when the test completes. */ + this._done = next; + + var sourcePipe = new CustomPipe(name + "-source"); + + /** The source of data for the copier to copy. */ + this._source = sourcePipe.inputStream; + + /** + * The sink to which to write data which will appear in the copier's source. + */ + this._copyableDataStream = sourcePipe.outputStream; + + var sinkPipe = new CustomPipe(name + "-sink"); + + /** The sink to which the copier copies data. */ + this._sink = sinkPipe.outputStream; + + /** Input stream from which to read data the copier's written to its sink. */ + this._copiedDataStream = sinkPipe.inputStream; + + this._copiedDataStream.disableReadabilityLimit(); + + /** + * True if there's a callback waiting to read data written by the copier to + * its output, from the input end of the pipe representing the copier's sink. + */ + this._waitingForData = false; + + /** + * An array of the bytes of data expected to be written to output by the + * copier when this test runs. + */ + this._expectedData = undefined; + + /** Array of bytes of data received so far. */ + this._receivedData = []; + + /** The expected final status returned by the copier. */ + this._expectedStatus = -1; + + /** The actual final status returned by the copier. */ + this._actualStatus = -1; + + /** The most recent sequence of bytes written to output by the copier. */ + this._lastQuantum = []; + + /** + * True iff we've received the last quantum of data written to the sink by the + * copier. + */ + this._allDataWritten = false; + + /** + * True iff the copier has notified its associated stream listener of + * completion. + */ + this._copyingFinished = false; + + /** Index of the next task to execute while driving the copier. */ + this._currentTask = 0; + + /** Array containing all tasks to run. */ + this._tasks = []; + + /** The copier used by this test. */ + this._copier = new WriteThroughCopier(this._source, this._sink, this, null); + + // Start watching for data written by the copier to the sink. + this._waitForWrittenData(); +} +CopyTest.prototype = { + /** + * Adds the given array of bytes to data in the copier's source. + * + * @param bytes : [uint] + * array of bytes of data to add to the source for the copier + */ + addToSource: function addToSource(bytes) { + var self = this; + this._addToTasks(function addToSourceTask() { + note("addToSourceTask"); + + try { + self._copyableDataStream.makeWritable(bytes.length); + self._copyableDataStream.writeByteArray(bytes); + } finally { + self._stageNextTask(); + } + }); + }, + + /** + * Makes bytes of data previously added to the source available to be read by + * the copier. + * + * @param count : uint + * number of bytes to make available for reading + */ + makeSourceReadable: function makeSourceReadable(count) { + var self = this; + this._addToTasks(function makeSourceReadableTask() { + note("makeSourceReadableTask"); + + self._source.makeReadable(count); + self._stageNextTask(); + }); + }, + + /** + * Increases available space in the sink by the given amount, waits for the + * given series of arrays of bytes to be written to sink by the copier, and + * causes execution to asynchronously continue to the next task when the last + * of those arrays of bytes is received. + * + * @param bytes : uint + * number of bytes of space to make available in the sink + * @param dataQuantums : [[uint]] + * array of byte arrays to expect to be written in sequence to the sink + */ + makeSinkWritableAndWaitFor: function makeSinkWritableAndWaitFor( + bytes, + dataQuantums + ) { + var self = this; + + Assert.equal( + bytes, + dataQuantums.reduce(function (partial, current) { + return partial + current.length; + }, 0), + "bytes/quantums mismatch" + ); + + function increaseSinkSpaceTask() { + /* Now do the actual work to trigger the interceptor. */ + self._sink.makeWritable(bytes); + } + + this._waitForHelper( + "increaseSinkSpaceTask", + dataQuantums, + increaseSinkSpaceTask + ); + }, + + /** + * Increases available space in the sink by the given amount, waits for the + * given series of arrays of bytes to be written to sink by the copier, and + * causes execution to asynchronously continue to the next task when the last + * of those arrays of bytes is received. + * + * @param bytes : uint + * number of bytes of space to make available in the sink + * @param dataQuantums : [[uint]] + * array of byte arrays to expect to be written in sequence to the sink + */ + makeSinkWritableByIncrementsAndWaitFor: + function makeSinkWritableByIncrementsAndWaitFor(bytes, dataQuantums) { + var self = this; + + var desiredAmounts = dataQuantums.map(function (v) { + return v.length; + }); + Assert.equal(bytes, sum(desiredAmounts), "bytes/quantums mismatch"); + + function increaseSinkSpaceByIncrementsTask() { + /* Now do the actual work to trigger the interceptor incrementally. */ + self._sink.makeWritableByIncrements(desiredAmounts); + } + + this._waitForHelper( + "increaseSinkSpaceByIncrementsTask", + dataQuantums, + increaseSinkSpaceByIncrementsTask + ); + }, + + /** + * Close the copier's source stream, then asynchronously continue to the next + * task. + * + * @param status : nsresult + * the status to provide when closing the copier's source stream + */ + closeSource: function closeSource(status) { + var self = this; + + this._addToTasks(function closeSourceTask() { + note("closeSourceTask"); + + self._source.closeWithStatus(status); + self._stageNextTask(); + }); + }, + + /** + * Close the copier's source stream, then wait for the given number of bytes + * and for the given series of arrays of bytes to be written to the sink, then + * asynchronously continue to the next task. + * + * @param status : nsresult + * the status to provide when closing the copier's source stream + * @param bytes : uint + * number of bytes of space to make available in the sink + * @param dataQuantums : [[uint]] + * array of byte arrays to expect to be written in sequence to the sink + */ + closeSourceAndWaitFor: function closeSourceAndWaitFor( + status, + bytes, + dataQuantums + ) { + var self = this; + + Assert.equal( + bytes, + sum( + dataQuantums.map(function (v) { + return v.length; + }) + ), + "bytes/quantums mismatch" + ); + + function closeSourceAndWaitForTask() { + self._sink.makeWritable(bytes); + self._copyableDataStream.closeWithStatus(status); + } + + this._waitForHelper( + "closeSourceAndWaitForTask", + dataQuantums, + closeSourceAndWaitForTask + ); + }, + + /** + * Closes the copier's sink stream, providing the given status, then + * asynchronously continue to the next task. + * + * @param status : nsresult + * the status to provide when closing the copier's sink stream + */ + closeSink: function closeSink(status) { + var self = this; + this._addToTasks(function closeSinkTask() { + note("closeSinkTask"); + + self._sink.closeWithStatus(status); + self._stageNextTask(); + }); + }, + + /** + * Closes the copier's source stream, then immediately closes the copier's + * sink stream, then asynchronously continues to the next task. + * + * @param sourceStatus : nsresult + * the status to provide when closing the copier's source stream + * @param sinkStatus : nsresult + * the status to provide when closing the copier's sink stream + */ + closeSourceThenSink: function closeSourceThenSink(sourceStatus, sinkStatus) { + var self = this; + this._addToTasks(function closeSourceThenSinkTask() { + note("closeSourceThenSinkTask"); + + self._source.closeWithStatus(sourceStatus); + self._sink.closeWithStatus(sinkStatus); + self._stageNextTask(); + }); + }, + + /** + * Closes the copier's sink stream, then immediately closes the copier's + * source stream, then asynchronously continues to the next task. + * + * @param sinkStatus : nsresult + * the status to provide when closing the copier's sink stream + * @param sourceStatus : nsresult + * the status to provide when closing the copier's source stream + */ + closeSinkThenSource: function closeSinkThenSource(sinkStatus, sourceStatus) { + var self = this; + this._addToTasks(function closeSinkThenSourceTask() { + note("closeSinkThenSource"); + + self._sink.closeWithStatus(sinkStatus); + self._source.closeWithStatus(sourceStatus); + self._stageNextTask(); + }); + }, + + /** + * Indicates that the given status is expected to be returned when the stream + * listener for the copy indicates completion, that the expected data copied + * by the copier to sink are the concatenation of the arrays of bytes in + * receivedData, and kicks off the tasks in this test. + * + * @param expectedStatus : nsresult + * the status expected to be returned by the copier at completion + * @param receivedData : [[uint]] + * an array containing arrays of bytes whose concatenation constitutes the + * expected copied data + */ + expect: function expect(expectedStatus, receivedData) { + this._expectedStatus = expectedStatus; + this._expectedData = []; + for (var i = 0, sz = receivedData.length; i < sz; i++) { + this._expectedData.push.apply(this._expectedData, receivedData[i]); + } + + this._stageNextTask(); + }, + + /** + * Sets up a stream interceptor that will verify that each piece of data + * written to the sink by the copier corresponds to the currently expected + * pieces of data, calls the trigger, then waits for those pieces of data to + * be received. Once all have been received, the interceptor is removed and + * the next task is asynchronously executed. + * + * @param name : string + * name of the task created by this, used in debugging output + * @param dataQuantums : [[uint]] + * array of expected arrays of bytes to be written to the sink by the copier + * @param trigger : function() : void + * function to call after setting up the interceptor to wait for + * notifications (which will be generated as a result of this function's + * actions) + */ + _waitForHelper: function _waitForHelper(name, dataQuantums, trigger) { + var self = this; + this._addToTasks(function waitForHelperTask() { + note(name); + + var quantumIndex = 0; + + /* + * Intercept all data-available notifications so we can continue when all + * the ones we expect have been received. + */ + var streamReadyCallback = { + onInputStreamReady: function wrapperOnInputStreamReady(input) { + dumpn( + "*** streamReadyCallback.onInputStreamReady" + + "(" + + input.name + + ")" + ); + + Assert.equal(this, streamReadyCallback, "sanity"); + + try { + if (quantumIndex < dataQuantums.length) { + var quantum = dataQuantums[quantumIndex++]; + var sz = quantum.length; + Assert.equal( + self._lastQuantum.length, + sz, + "different quantum lengths" + ); + for (var i = 0; i < sz; i++) { + Assert.equal( + self._lastQuantum[i], + quantum[i], + "bad data at " + i + ); + } + + dumpn( + "*** waiting to check remaining " + + (dataQuantums.length - quantumIndex) + + " quantums..." + ); + } + } finally { + if (quantumIndex === dataQuantums.length) { + dumpn("*** data checks completed! next task..."); + self._copiedDataStream.removeStreamReadyInterceptor(); + self._stageNextTask(); + } + } + }, + }; + + var interceptor = createStreamReadyInterceptor( + streamReadyCallback, + "onInputStreamReady" + ); + self._copiedDataStream.interceptStreamReadyCallbacks(interceptor); + + /* Do the deed. */ + trigger(); + }); + }, + + /** + * Initiates asynchronous waiting for data written to the copier's sink to be + * available for reading from the input end of the sink's pipe. The callback + * stores the received data for comparison in the interceptor used in the + * callback added by _waitForHelper and signals test completion when it + * receives a zero-data-available notification (if the copier has notified + * that it is finished; otherwise allows execution to continue until that has + * occurred). + */ + _waitForWrittenData: function _waitForWrittenData() { + dumpn("*** _waitForWrittenData (" + this.name + ")"); + + var self = this; + var outputWrittenWatcher = { + onInputStreamReady: function onInputStreamReady(input) { + dumpn( + // eslint-disable-next-line no-useless-concat + "*** outputWrittenWatcher.onInputStreamReady" + "(" + input.name + ")" + ); + + if (self._allDataWritten) { + do_throw( + "ruh-roh! why are we getting notified of more data " + + "after we should have received all of it?" + ); + } + + self._waitingForData = false; + + try { + var avail = input.available(); + } catch (e) { + dumpn("*** available() threw! error: " + e); + if (self._completed) { + dumpn( + "*** NB: this isn't a problem, because we've copied " + + "completely now, and this notify may have been expedited " + + "by maybeNotifyFinally such that we're being called when " + + "we can *guarantee* nothing is available any more" + ); + } + avail = 0; + } + + if (avail > 0) { + var data = input.readByteArray(avail); + Assert.equal( + data.length, + avail, + "readByteArray returned wrong number of bytes?" + ); + self._lastQuantum = data; + self._receivedData.push.apply(self._receivedData, data); + } + + if (avail === 0) { + dumpn("*** all data received!"); + + self._allDataWritten = true; + + if (self._copyingFinished) { + dumpn("*** copying already finished, continuing to next test"); + self._testComplete(); + } else { + dumpn("*** copying not finished, waiting for that to happen"); + } + + return; + } + + self._waitForWrittenData(); + }, + }; + + this._copiedDataStream.asyncWait( + outputWrittenWatcher, + 0, + 1, + gThreadManager.currentThread + ); + this._waitingForData = true; + }, + + /** + * Indicates this test is complete, does the final data-received and copy + * status comparisons, and calls the test-completion function provided when + * this test was first created. + */ + _testComplete: function _testComplete() { + dumpn("*** CopyTest(" + this.name + ") complete! On to the next test..."); + + try { + Assert.ok(this._allDataWritten, "expect all data written now!"); + Assert.ok(this._copyingFinished, "expect copying finished now!"); + + Assert.equal( + this._actualStatus, + this._expectedStatus, + "wrong final status" + ); + + var expected = this._expectedData, + received = this._receivedData; + dumpn("received: [" + received + "], expected: [" + expected + "]"); + Assert.equal(received.length, expected.length, "wrong data"); + for (var i = 0, sz = expected.length; i < sz; i++) { + Assert.equal(received[i], expected[i], "bad data at " + i); + } + } catch (e) { + dumpn("!!! ERROR PERFORMING FINAL " + this.name + " CHECKS! " + e); + throw e; + } finally { + dumpn( + "*** CopyTest(" + + this.name + + ") complete! " + + "Invoking test-completion callback..." + ); + this._done(); + } + }, + + /** Dispatches an event at this thread which will run the next task. */ + _stageNextTask: function _stageNextTask() { + dumpn("*** CopyTest(" + this.name + ")._stageNextTask()"); + + if (this._currentTask === this._tasks.length) { + dumpn("*** CopyTest(" + this.name + ") tasks complete!"); + return; + } + + var task = this._tasks[this._currentTask++]; + var event = { + run: function run() { + try { + task(); + } catch (e) { + do_throw("exception thrown running task: " + e); + } + }, + }; + gThreadManager.dispatchToMainThread(event); + }, + + /** + * Adds the given function as a task to be run at a later time. + * + * @param task : function() : void + * the function to call as a task + */ + _addToTasks: function _addToTasks(task) { + this._tasks.push(task); + }, + + // + // see nsIRequestObserver.onStartRequest + // + onStartRequest: function onStartRequest(self) { + dumpn("*** CopyTest.onStartRequest (" + self.name + ")"); + + Assert.equal(this._receivedData.length, 0); + Assert.equal(this._lastQuantum.length, 0); + }, + + // + // see nsIRequestObserver.onStopRequest + // + onStopRequest: function onStopRequest(self, status) { + dumpn("*** CopyTest.onStopRequest (" + self.name + ", " + status + ")"); + + this._actualStatus = status; + + this._copyingFinished = true; + + if (this._allDataWritten) { + dumpn("*** all data written, continuing with remaining tests..."); + this._testComplete(); + } else { + /* + * Everything's copied as far as the copier is concerned. However, there + * may be a backup transferring from the output end of the copy sink to + * the input end where we can actually verify that the expected data was + * written as expected, because that transfer occurs asynchronously. If + * we do final data-received checks now, we'll miss still-pending data. + * Therefore, to wrap up this copy test we still need to asynchronously + * wait on the input end of the sink until we hit end-of-stream or some + * error condition. Then we know we're done and can continue with the + * next test. + */ + dumpn("*** not all data copied, waiting for that to happen..."); + + if (!this._waitingForData) { + this._waitForWrittenData(); + } + + this._copiedDataStream.maybeNotifyFinally(); + } + }, +}; diff --git a/netwerk/test/httpserver/test/test_basic_functionality.js b/netwerk/test/httpserver/test/test_basic_functionality.js new file mode 100644 index 0000000000..d00cfa4eb2 --- /dev/null +++ b/netwerk/test/httpserver/test/test_basic_functionality.js @@ -0,0 +1,182 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* + * Basic functionality test, from the client programmer's POV. + */ + +XPCOMUtils.defineLazyGetter(this, "port", function () { + return srv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test( + "http://localhost:" + port + "/objHandler", + null, + start_objHandler, + null + ), + new Test( + "http://localhost:" + port + "/functionHandler", + null, + start_functionHandler, + null + ), + new Test( + "http://localhost:" + port + "/nonexistent-path", + null, + start_non_existent_path, + null + ), + new Test( + "http://localhost:" + port + "/lotsOfHeaders", + null, + start_lots_of_headers, + null + ), + ]; +}); + +var srv; + +function run_test() { + srv = createServer(); + + // base path + // XXX should actually test this works with a file by comparing streams! + var path = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + srv.registerDirectory("/", path); + + // register a few test paths + srv.registerPathHandler("/objHandler", objHandler); + srv.registerPathHandler("/functionHandler", functionHandler); + srv.registerPathHandler("/lotsOfHeaders", lotsOfHeadersHandler); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +const HEADER_COUNT = 1000; + +// TEST DATA + +// common properties *always* appended by server +// or invariants for every URL in paths +function commonCheck(ch) { + Assert.ok(ch.contentLength > -1); + Assert.equal(ch.getResponseHeader("connection"), "close"); + Assert.ok(!ch.isNoStoreResponse()); + Assert.ok(!ch.isPrivateResponse()); +} + +function start_objHandler(ch) { + commonCheck(ch); + + Assert.equal(ch.responseStatus, 200); + Assert.ok(ch.requestSucceeded); + Assert.equal(ch.getResponseHeader("content-type"), "text/plain"); + Assert.equal(ch.responseStatusText, "OK"); + + var reqMin = {}, + reqMaj = {}, + respMin = {}, + respMaj = {}; + ch.getRequestVersion(reqMaj, reqMin); + ch.getResponseVersion(respMaj, respMin); + Assert.ok(reqMaj.value == respMaj.value && reqMin.value == respMin.value); +} + +function start_functionHandler(ch) { + commonCheck(ch); + + Assert.equal(ch.responseStatus, 404); + Assert.ok(!ch.requestSucceeded); + Assert.equal(ch.getResponseHeader("foopy"), "quux-baz"); + Assert.equal(ch.responseStatusText, "Page Not Found"); + + var reqMin = {}, + reqMaj = {}, + respMin = {}, + respMaj = {}; + ch.getRequestVersion(reqMaj, reqMin); + ch.getResponseVersion(respMaj, respMin); + Assert.ok(reqMaj.value == 1 && reqMin.value == 1); + Assert.ok(respMaj.value == 1 && respMin.value == 1); +} + +function start_non_existent_path(ch) { + commonCheck(ch); + + Assert.equal(ch.responseStatus, 404); + Assert.ok(!ch.requestSucceeded); +} + +function start_lots_of_headers(ch) { + commonCheck(ch); + + Assert.equal(ch.responseStatus, 200); + Assert.ok(ch.requestSucceeded); + + for (var i = 0; i < HEADER_COUNT; i++) { + Assert.equal(ch.getResponseHeader("X-Header-" + i), "value " + i); + } +} + +// PATH HANDLERS + +// /objHandler +var objHandler = { + handle(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + + var body = "Request (slightly reformatted):\n\n"; + body += metadata.method + " " + metadata.path; + + Assert.equal(metadata.port, port); + + if (metadata.queryString) { + body += "?" + metadata.queryString; + } + + body += " HTTP/" + metadata.httpVersion + "\n"; + + var headEnum = metadata.headers; + while (headEnum.hasMoreElements()) { + var fieldName = headEnum + .getNext() + .QueryInterface(Ci.nsISupportsString).data; + body += fieldName + ": " + metadata.getHeader(fieldName) + "\n"; + } + + response.bodyOutputStream.write(body, body.length); + }, + QueryInterface: ChromeUtils.generateQI(["nsIHttpRequestHandler"]), +}; + +// /functionHandler +function functionHandler(metadata, response) { + response.setStatusLine("1.1", 404, "Page Not Found"); + response.setHeader("foopy", "quux-baz", false); + + Assert.equal(metadata.port, port); + Assert.equal(metadata.host, "localhost"); + Assert.equal(metadata.path.charAt(0), "/"); + + var body = "this is text\n"; + response.bodyOutputStream.write(body, body.length); +} + +// /lotsOfHeaders +function lotsOfHeadersHandler(request, response) { + response.setHeader("Content-Type", "text/plain", false); + + for (var i = 0; i < HEADER_COUNT; i++) { + response.setHeader("X-Header-" + i, "value " + i, false); + } +} diff --git a/netwerk/test/httpserver/test/test_body_length.js b/netwerk/test/httpserver/test/test_body_length.js new file mode 100644 index 0000000000..dba81df619 --- /dev/null +++ b/netwerk/test/httpserver/test/test_body_length.js @@ -0,0 +1,68 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* + * Tests that the Content-Length header in incoming requests is interpreted as + * a decimal number, even if it has the form (including leading zero) of an + * octal number. + */ + +var srv; + +function run_test() { + srv = createServer(); + srv.registerPathHandler("/content-length", contentLength); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +const REQUEST_DATA = "12345678901234567"; + +function contentLength(request, response) { + Assert.equal(request.method, "POST"); + Assert.equal(request.getHeader("Content-Length"), "017"); + + var body = new ScriptableInputStream(request.bodyInputStream); + + var avail; + var data = ""; + while ((avail = body.available()) > 0) { + data += body.read(avail); + } + + Assert.equal(data, REQUEST_DATA); +} + +/** ************* + * BEGIN TESTS * + ***************/ + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test( + "http://localhost:" + srv.identity.primaryPort + "/content-length", + init_content_length + ), + ]; +}); + +function init_content_length(ch) { + var content = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + content.data = REQUEST_DATA; + + ch.QueryInterface(Ci.nsIUploadChannel).setUploadStream( + content, + "text/plain", + REQUEST_DATA.length + ); + + // Override the values implicitly set by setUploadStream above. + ch.requestMethod = "POST"; + ch.setRequestHeader("Content-Length", "017", false); // 17 bytes, not 15 +} diff --git a/netwerk/test/httpserver/test/test_byte_range.js b/netwerk/test/httpserver/test/test_byte_range.js new file mode 100644 index 0000000000..94e624bb90 --- /dev/null +++ b/netwerk/test/httpserver/test/test_byte_range.js @@ -0,0 +1,272 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// checks if a byte range request and non-byte range request retrieve the +// correct data. + +var srv; +XPCOMUtils.defineLazyGetter(this, "PREFIX", function () { + return "http://localhost:" + srv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test( + PREFIX + "/range.txt", + init_byterange, + start_byterange, + stop_byterange + ), + new Test(PREFIX + "/range.txt", init_byterange2, start_byterange2), + new Test( + PREFIX + "/range.txt", + init_byterange3, + start_byterange3, + stop_byterange3 + ), + new Test(PREFIX + "/range.txt", init_byterange4, start_byterange4), + new Test( + PREFIX + "/range.txt", + init_byterange5, + start_byterange5, + stop_byterange5 + ), + new Test( + PREFIX + "/range.txt", + init_byterange6, + start_byterange6, + stop_byterange6 + ), + new Test( + PREFIX + "/range.txt", + init_byterange7, + start_byterange7, + stop_byterange7 + ), + new Test( + PREFIX + "/range.txt", + init_byterange8, + start_byterange8, + stop_byterange8 + ), + new Test( + PREFIX + "/range.txt", + init_byterange9, + start_byterange9, + stop_byterange9 + ), + new Test(PREFIX + "/range.txt", init_byterange10, start_byterange10), + new Test( + PREFIX + "/range.txt", + init_byterange11, + start_byterange11, + stop_byterange11 + ), + new Test(PREFIX + "/empty.txt", null, start_byterange12, stop_byterange12), + new Test( + PREFIX + "/headers.txt", + init_byterange13, + start_byterange13, + null + ), + new Test(PREFIX + "/range.txt", null, start_normal, stop_normal), + ]; +}); + +function run_test() { + srv = createServer(); + var dir = do_get_file("data/ranges/"); + srv.registerDirectory("/", dir); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +function start_normal(ch) { + Assert.equal(ch.responseStatus, 200); + Assert.equal(ch.getResponseHeader("Content-Length"), "21"); + Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain"); +} + +function stop_normal(ch, status, data) { + Assert.equal(data.length, 21); + Assert.equal(data[0], 0x54); + Assert.equal(data[20], 0x0a); +} + +function init_byterange(ch) { + ch.setRequestHeader("Range", "bytes=10-", false); +} + +function start_byterange(ch) { + Assert.equal(ch.responseStatus, 206); + Assert.equal(ch.getResponseHeader("Content-Length"), "11"); + Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain"); + Assert.equal(ch.getResponseHeader("Content-Range"), "bytes 10-20/21"); +} + +function stop_byterange(ch, status, data) { + Assert.equal(data.length, 11); + Assert.equal(data[0], 0x64); + Assert.equal(data[10], 0x0a); +} + +function init_byterange2(ch) { + ch.setRequestHeader("Range", "bytes=21-", false); +} + +function start_byterange2(ch) { + Assert.equal(ch.responseStatus, 416); +} + +function init_byterange3(ch) { + ch.setRequestHeader("Range", "bytes=10-15", false); +} + +function start_byterange3(ch) { + Assert.equal(ch.responseStatus, 206); + Assert.equal(ch.getResponseHeader("Content-Length"), "6"); + Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain"); + Assert.equal(ch.getResponseHeader("Content-Range"), "bytes 10-15/21"); +} + +function stop_byterange3(ch, status, data) { + Assert.equal(data.length, 6); + Assert.equal(data[0], 0x64); + Assert.equal(data[1], 0x20); + Assert.equal(data[2], 0x62); + Assert.equal(data[3], 0x65); + Assert.equal(data[4], 0x20); + Assert.equal(data[5], 0x73); +} + +function init_byterange4(ch) { + ch.setRequestHeader("Range", "xbytes=21-", false); +} + +function start_byterange4(ch) { + Assert.equal(ch.responseStatus, 400); +} + +function init_byterange5(ch) { + ch.setRequestHeader("Range", "bytes=-5", false); +} + +function start_byterange5(ch) { + Assert.equal(ch.responseStatus, 206); +} + +function stop_byterange5(ch, status, data) { + Assert.equal(data.length, 5); + Assert.equal(data[0], 0x65); + Assert.equal(data[1], 0x65); + Assert.equal(data[2], 0x6e); + Assert.equal(data[3], 0x2e); + Assert.equal(data[4], 0x0a); +} + +function init_byterange6(ch) { + ch.setRequestHeader("Range", "bytes=15-12", false); +} + +function start_byterange6(ch) { + Assert.equal(ch.responseStatus, 200); +} + +function stop_byterange6(ch, status, data) { + Assert.equal(data.length, 21); + Assert.equal(data[0], 0x54); + Assert.equal(data[20], 0x0a); +} + +function init_byterange7(ch) { + ch.setRequestHeader("Range", "bytes=0-5", false); +} + +function start_byterange7(ch) { + Assert.equal(ch.responseStatus, 206); + Assert.equal(ch.getResponseHeader("Content-Length"), "6"); + Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain"); + Assert.equal(ch.getResponseHeader("Content-Range"), "bytes 0-5/21"); +} + +function stop_byterange7(ch, status, data) { + Assert.equal(data.length, 6); + Assert.equal(data[0], 0x54); + Assert.equal(data[1], 0x68); + Assert.equal(data[2], 0x69); + Assert.equal(data[3], 0x73); + Assert.equal(data[4], 0x20); + Assert.equal(data[5], 0x73); +} + +function init_byterange8(ch) { + ch.setRequestHeader("Range", "bytes=20-21", false); +} + +function start_byterange8(ch) { + Assert.equal(ch.responseStatus, 206); + Assert.equal(ch.getResponseHeader("Content-Range"), "bytes 20-20/21"); +} + +function stop_byterange8(ch, status, data) { + Assert.equal(data.length, 1); + Assert.equal(data[0], 0x0a); +} + +function init_byterange9(ch) { + ch.setRequestHeader("Range", "bytes=020-021", false); +} + +function start_byterange9(ch) { + Assert.equal(ch.responseStatus, 206); +} + +function stop_byterange9(ch, status, data) { + Assert.equal(data.length, 1); + Assert.equal(data[0], 0x0a); +} + +function init_byterange10(ch) { + ch.setRequestHeader("Range", "bytes=-", false); +} + +function start_byterange10(ch) { + Assert.equal(ch.responseStatus, 400); +} + +function init_byterange11(ch) { + ch.setRequestHeader("Range", "bytes=-500", false); +} + +function start_byterange11(ch) { + Assert.equal(ch.responseStatus, 206); +} + +function stop_byterange11(ch, status, data) { + Assert.equal(data.length, 21); + Assert.equal(data[0], 0x54); + Assert.equal(data[20], 0x0a); +} + +function start_byterange12(ch) { + Assert.equal(ch.responseStatus, 200); + Assert.equal(ch.getResponseHeader("Content-Length"), "0"); +} + +function stop_byterange12(ch, status, data) { + Assert.equal(data.length, 0); +} + +function init_byterange13(ch) { + ch.setRequestHeader("Range", "bytes=9999999-", false); +} + +function start_byterange13(ch) { + Assert.equal(ch.responseStatus, 416); + Assert.equal(ch.getResponseHeader("X-SJS-Header"), "customized"); +} diff --git a/netwerk/test/httpserver/test/test_cern_meta.js b/netwerk/test/httpserver/test/test_cern_meta.js new file mode 100644 index 0000000000..b6c3047685 --- /dev/null +++ b/netwerk/test/httpserver/test/test_cern_meta.js @@ -0,0 +1,79 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// exercises support for mod_cern_meta-style header/status line modification +var srv; + +XPCOMUtils.defineLazyGetter(this, "PREFIX", function () { + return "http://localhost:" + srv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test(PREFIX + "/test_both.html", null, start_testBoth, null), + new Test( + PREFIX + "/test_ctype_override.txt", + null, + start_test_ctype_override_txt, + null + ), + new Test( + PREFIX + "/test_status_override.html", + null, + start_test_status_override_html, + null + ), + new Test( + PREFIX + "/test_status_override_nodesc.txt", + null, + start_test_status_override_nodesc_txt, + null + ), + new Test(PREFIX + "/caret_test.txt^", null, start_caret_test_txt_, null), + ]; +}); + +function run_test() { + srv = createServer(); + + var cernDir = do_get_file("data/cern_meta/"); + srv.registerDirectory("/", cernDir); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +// TEST DATA + +function start_testBoth(ch) { + Assert.equal(ch.responseStatus, 501); + Assert.equal(ch.responseStatusText, "Unimplemented"); + + Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain"); +} + +function start_test_ctype_override_txt(ch) { + Assert.equal(ch.getResponseHeader("Content-Type"), "text/html"); +} + +function start_test_status_override_html(ch) { + Assert.equal(ch.responseStatus, 404); + Assert.equal(ch.responseStatusText, "Can't Find This"); +} + +function start_test_status_override_nodesc_txt(ch) { + Assert.equal(ch.responseStatus, 732); + Assert.equal(ch.responseStatusText, ""); +} + +function start_caret_test_txt_(ch) { + Assert.equal(ch.responseStatus, 500); + Assert.equal(ch.responseStatusText, "This Isn't A Server Error"); + + Assert.equal(ch.getResponseHeader("Foo-RFC"), "3092"); + Assert.equal(ch.getResponseHeader("Shaving-Cream-Atom"), "Illudium Phosdex"); +} diff --git a/netwerk/test/httpserver/test/test_default_index_handler.js b/netwerk/test/httpserver/test/test_default_index_handler.js new file mode 100644 index 0000000000..3ee25e95d1 --- /dev/null +++ b/netwerk/test/httpserver/test/test_default_index_handler.js @@ -0,0 +1,248 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// checks for correct output with the default index handler, mostly to do +// escaping checks -- highly dependent on the default index handler output +// format + +var srv, dir, gDirEntries; + +XPCOMUtils.defineLazyGetter(this, "BASE_URL", function () { + return "http://localhost:" + srv.identity.primaryPort + "/"; +}); + +function run_test() { + createTestDirectory(); + + srv = createServer(); + srv.registerDirectory("/", dir); + + var nameDir = do_get_file("data/name-scheme/"); + srv.registerDirectory("/bar/", nameDir); + + srv.start(-1); + + function done() { + do_test_pending(); + destroyTestDirectory(); + srv.stop(function () { + do_test_finished(); + }); + } + + runHttpTests(tests, done); +} + +function createTestDirectory() { + dir = Services.dirsvc.get("TmpD", Ci.nsIFile); + dir.append("index_handler_test_" + Math.random()); + dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + + // populate with test directories, files, etc. + // Files must be in expected order of display on the index page! + + var files = []; + + makeFile("aa_directory", true, dir, files); + makeFile("Ba_directory", true, dir, files); + makeFile("bb_directory", true, dir, files); + makeFile("foo", true, dir, files); + makeFile("a_file", false, dir, files); + makeFile("B_file", false, dir, files); + makeFile("za'z", false, dir, files); + makeFile("zb&z", false, dir, files); + makeFile("zc<q", false, dir, files); + makeFile('zd"q', false, dir, files); + makeFile("ze%g", false, dir, files); + makeFile("zf%200h", false, dir, files); + makeFile("zg>m", false, dir, files); + + gDirEntries = [files]; + + var subdir = dir.clone(); + subdir.append("foo"); + + files = []; + + makeFile("aa_dir", true, subdir, files); + makeFile("b_dir", true, subdir, files); + makeFile("AA_file.txt", false, subdir, files); + makeFile("test.txt", false, subdir, files); + + gDirEntries.push(files); +} + +function destroyTestDirectory() { + dir.remove(true); +} + +/** *********** + * UTILITIES * + *************/ + +/** Verifies data in bytes for the trailing-caret path above. */ +function hiddenDataCheck(bytes, uri, path) { + var data = String.fromCharCode.apply(null, bytes); + + var parser = new DOMParser(); + + // Note: the index format isn't XML -- it's actually HTML -- but we require + // the index format also be valid XML, albeit XML without namespaces, + // XML declarations, etc. Doing this simplifies output checking. + try { + var doc = parser.parseFromString(data, "application/xml"); + } catch (e) { + do_throw("document failed to parse as XML"); + } + + var body = doc.documentElement.getElementsByTagName("body"); + Assert.equal(body.length, 1); + body = body[0]; + + // header + var header = body.getElementsByTagName("h1"); + Assert.equal(header.length, 1); + + Assert.equal(header[0].textContent, path); + + // files + var lst = body.getElementsByTagName("ol"); + Assert.equal(lst.length, 1); + var items = lst[0].getElementsByTagName("li"); + + var top = Services.io.newURI(uri); + + // N.B. No ERROR_IF_SEE_THIS.txt^ file! + var dirEntries = [ + { name: "file.txt", isDirectory: false }, + { name: "SHOULD_SEE_THIS.txt^", isDirectory: false }, + ]; + + for (var i = 0; i < items.length; i++) { + var link = items[i].childNodes[0]; + var f = dirEntries[i]; + + var sep = f.isDirectory ? "/" : ""; + + Assert.equal(link.textContent, f.name + sep); + + uri = Services.io.newURI(link.getAttribute("href"), null, top); + Assert.equal(decodeURIComponent(uri.pathQueryRef), path + f.name + sep); + } +} + +/** + * Verifies data in bytes (an array of bytes) represents an index page for the + * given URI and path, which should be a page listing the given directory + * entries, in order. + * + * @param bytes + * array of bytes representing the index page's contents + * @param uri + * string which is the URI of the index page + * @param path + * the path portion of uri + * @param dirEntries + * sorted (in the manner the directory entries should be sorted) array of + * objects, each of which has a name property (whose value is the file's name, + * without / if it's a directory) and an isDirectory property (with expected + * value) + */ +function dataCheck(bytes, uri, path, dirEntries) { + var data = String.fromCharCode.apply(null, bytes); + + var parser = new DOMParser(); + + // Note: the index format isn't XML -- it's actually HTML -- but we require + // the index format also be valid XML, albeit XML without namespaces, + // XML declarations, etc. Doing this simplifies output checking. + try { + var doc = parser.parseFromString(data, "application/xml"); + } catch (e) { + do_throw("document failed to parse as XML"); + } + + var body = doc.documentElement.getElementsByTagName("body"); + Assert.equal(body.length, 1); + body = body[0]; + + // header + var header = body.getElementsByTagName("h1"); + Assert.equal(header.length, 1); + + Assert.equal(header[0].textContent, path); + + // files + var lst = body.getElementsByTagName("ol"); + Assert.equal(lst.length, 1); + var items = lst[0].getElementsByTagName("li"); + var top = Services.io.newURI(uri); + + for (var i = 0; i < items.length; i++) { + var link = items[i].childNodes[0]; + var f = dirEntries[i]; + + var sep = f.isDirectory ? "/" : ""; + + Assert.equal(link.textContent, f.name + sep); + + uri = Services.io.newURI(link.getAttribute("href"), null, top); + Assert.equal(decodeURIComponent(uri.pathQueryRef), path + f.name + sep); + } +} + +/** + * Create a file/directory with the given name underneath parentDir, and + * append an object with name/isDirectory properties to lst corresponding + * to it if the file/directory could be created. + */ +function makeFile(name, isDirectory, parentDir, lst) { + var type = Ci.nsIFile[isDirectory ? "DIRECTORY_TYPE" : "NORMAL_FILE_TYPE"]; + var file = parentDir.clone(); + + try { + file.append(name); + file.create(type, 0o755); + lst.push({ name, isDirectory }); + } catch (e) { + /* OS probably doesn't like file name, skip */ + } +} + +/** ******* + * TESTS * + *********/ + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test(BASE_URL, null, start, stopRootDirectory), + new Test(BASE_URL + "foo/", null, start, stopFooDirectory), + new Test( + BASE_URL + "bar/folder^/", + null, + start, + stopTrailingCaretDirectory + ), + ]; +}); + +// check top-level directory listing +function start(ch) { + Assert.equal(ch.getResponseHeader("Content-Type"), "text/html;charset=utf-8"); +} +function stopRootDirectory(ch, status, data) { + dataCheck(data, BASE_URL, "/", gDirEntries[0]); +} + +// check non-top-level, too +function stopFooDirectory(ch, status, data) { + dataCheck(data, BASE_URL + "foo/", "/foo/", gDirEntries[1]); +} + +// trailing-caret leaf with hidden files +function stopTrailingCaretDirectory(ch, status, data) { + hiddenDataCheck(data, BASE_URL + "bar/folder^/", "/bar/folder^/"); +} diff --git a/netwerk/test/httpserver/test/test_empty_body.js b/netwerk/test/httpserver/test/test_empty_body.js new file mode 100644 index 0000000000..9e4a2fbdab --- /dev/null +++ b/netwerk/test/httpserver/test/test_empty_body.js @@ -0,0 +1,59 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// in its original incarnation, the server didn't like empty response-bodies; +// see the comment in _end for details + +var srv; + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test( + "http://localhost:" + srv.identity.primaryPort + "/empty-body-unwritten", + null, + ensureEmpty, + null + ), + new Test( + "http://localhost:" + srv.identity.primaryPort + "/empty-body-written", + null, + ensureEmpty, + null + ), + ]; +}); + +function run_test() { + srv = createServer(); + + // register a few test paths + srv.registerPathHandler("/empty-body-unwritten", emptyBodyUnwritten); + srv.registerPathHandler("/empty-body-written", emptyBodyWritten); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +// TEST DATA + +function ensureEmpty(ch) { + Assert.ok(ch.contentLength == 0); +} + +// PATH HANDLERS + +// /empty-body-unwritten +function emptyBodyUnwritten(metadata, response) { + response.setStatusLine("1.1", 200, "OK"); +} + +// /empty-body-written +function emptyBodyWritten(metadata, response) { + response.setStatusLine("1.1", 200, "OK"); + var body = ""; + response.bodyOutputStream.write(body, body.length); +} diff --git a/netwerk/test/httpserver/test/test_errorhandler_exception.js b/netwerk/test/httpserver/test/test_errorhandler_exception.js new file mode 100644 index 0000000000..25ccbc9700 --- /dev/null +++ b/netwerk/test/httpserver/test/test_errorhandler_exception.js @@ -0,0 +1,95 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// Request handlers may throw exceptions, and those exception should be caught +// by the server and converted into the proper error codes. + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test( + "http://localhost:" + srv.identity.primaryPort + "/throws/exception", + null, + start_throws_exception, + succeeded + ), + new Test( + "http://localhost:" + + srv.identity.primaryPort + + "/this/file/does/not/exist/and/404s", + null, + start_nonexistent_404_fails_so_400, + succeeded + ), + new Test( + "http://localhost:" + + srv.identity.primaryPort + + "/attempts/404/fails/so/400/fails/so/500s", + register400Handler, + start_multiple_exceptions_500, + succeeded + ), + ]; +}); + +var srv; + +function run_test() { + srv = createServer(); + + srv.registerErrorHandler(404, throwsException); + srv.registerPathHandler("/throws/exception", throwsException); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +// TEST DATA + +function checkStatusLine( + channel, + httpMaxVer, + httpMinVer, + httpCode, + statusText +) { + Assert.equal(channel.responseStatus, httpCode); + Assert.equal(channel.responseStatusText, statusText); + + var respMaj = {}, + respMin = {}; + channel.getResponseVersion(respMaj, respMin); + Assert.equal(respMaj.value, httpMaxVer); + Assert.equal(respMin.value, httpMinVer); +} + +function start_throws_exception(ch) { + checkStatusLine(ch, 1, 1, 500, "Internal Server Error"); +} + +function start_nonexistent_404_fails_so_400(ch) { + checkStatusLine(ch, 1, 1, 400, "Bad Request"); +} + +function start_multiple_exceptions_500(ch) { + checkStatusLine(ch, 1, 1, 500, "Internal Server Error"); +} + +function succeeded(ch, status, data) { + Assert.ok(Components.isSuccessCode(status)); +} + +function register400Handler(ch) { + srv.registerErrorHandler(400, throwsException); +} + +// PATH HANDLERS + +// /throws/exception (and also a 404 and 400 error handler) +function throwsException(metadata, response) { + throw new Error("this shouldn't cause an exit..."); + do_throw("Not reached!"); // eslint-disable-line no-unreachable +} diff --git a/netwerk/test/httpserver/test/test_header_array.js b/netwerk/test/httpserver/test/test_header_array.js new file mode 100644 index 0000000000..18a08cce51 --- /dev/null +++ b/netwerk/test/httpserver/test/test_header_array.js @@ -0,0 +1,66 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// test that special headers are sent as an array of headers with the same name + +var srv; + +function run_test() { + srv; + + srv = createServer(); + srv.registerPathHandler("/path-handler", pathHandler); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +/** ********** + * HANDLERS * + ************/ + +function pathHandler(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + response.setHeader("Proxy-Authenticate", "First line 1", true); + response.setHeader("Proxy-Authenticate", "Second line 1", true); + response.setHeader("Proxy-Authenticate", "Third line 1", true); + + response.setHeader("WWW-Authenticate", "Not merged line 1", false); + response.setHeader("WWW-Authenticate", "Not merged line 2", true); + + response.setHeader("WWW-Authenticate", "First line 2", false); + response.setHeader("WWW-Authenticate", "Second line 2", true); + response.setHeader("WWW-Authenticate", "Third line 2", true); + + response.setHeader("X-Single-Header-Merge", "Single 1", true); + response.setHeader("X-Single-Header-Merge", "Single 2", true); +} + +/** ************* + * BEGIN TESTS * + ***************/ + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test( + "http://localhost:" + srv.identity.primaryPort + "/path-handler", + null, + check + ), + ]; +}); + +function check(ch) { + var headerValue; + + headerValue = ch.getResponseHeader("Proxy-Authenticate"); + Assert.equal(headerValue, "First line 1\nSecond line 1\nThird line 1"); + headerValue = ch.getResponseHeader("WWW-Authenticate"); + Assert.equal(headerValue, "First line 2\nSecond line 2\nThird line 2"); + headerValue = ch.getResponseHeader("X-Single-Header-Merge"); + Assert.equal(headerValue, "Single 1,Single 2"); +} diff --git a/netwerk/test/httpserver/test/test_headers.js b/netwerk/test/httpserver/test/test_headers.js new file mode 100644 index 0000000000..8e920c6f2f --- /dev/null +++ b/netwerk/test/httpserver/test/test_headers.js @@ -0,0 +1,169 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// tests for header storage in httpd.js; nsHttpHeaders is an *internal* data +// structure and is not to be used directly outside of httpd.js itself except +// for testing purposes + +/** + * Ensures that a fieldname-fieldvalue combination is a valid header. + * + * @param fieldName + * the name of the header + * @param fieldValue + * the value of the header + * @param headers + * an nsHttpHeaders object to use to check validity + */ +function assertValidHeader(fieldName, fieldValue, headers) { + try { + headers.setHeader(fieldName, fieldValue, false); + } catch (e) { + do_throw("Unexpected exception thrown: " + e); + } +} + +/** + * Ensures that a fieldname-fieldvalue combination is not a valid header. + * + * @param fieldName + * the name of the header + * @param fieldValue + * the value of the header + * @param headers + * an nsHttpHeaders object to use to check validity + */ +function assertInvalidHeader(fieldName, fieldValue, headers) { + try { + headers.setHeader(fieldName, fieldValue, false); + throw new Error( + `Setting (${fieldName}, ${fieldValue}) as header succeeded!` + ); + } catch (e) { + if (e.result !== Cr.NS_ERROR_INVALID_ARG) { + do_throw("Unexpected exception thrown: " + e); + } + } +} + +function run_test() { + testHeaderValidity(); + testGetHeader(); + testHeaderEnumerator(); + testHasHeader(); +} + +function testHeaderValidity() { + var headers = new nsHttpHeaders(); + + assertInvalidHeader("f o", "bar", headers); + assertInvalidHeader("f\0n", "bar", headers); + assertInvalidHeader("foo:", "bar", headers); + assertInvalidHeader("f\\o", "bar", headers); + assertInvalidHeader("@xml", "bar", headers); + assertInvalidHeader("fiz(", "bar", headers); + assertInvalidHeader("HTTP/1.1", "bar", headers); + assertInvalidHeader('b"b', "bar", headers); + assertInvalidHeader("ascsd\t", "bar", headers); + assertInvalidHeader("{fds", "bar", headers); + assertInvalidHeader("baz?", "bar", headers); + assertInvalidHeader("a\\b\\c", "bar", headers); + assertInvalidHeader("\0x7F", "bar", headers); + assertInvalidHeader("\0x1F", "bar", headers); + assertInvalidHeader("f\n", "bar", headers); + assertInvalidHeader("foo", "b\nar", headers); + assertInvalidHeader("foo", "b\rar", headers); + assertInvalidHeader("foo", "b\0", headers); + + // request splitting, fwiw -- we're actually immune to this type of attack so + // long as we don't implement persistent connections + assertInvalidHeader("f\r\nGET /badness HTTP/1.1\r\nFoo", "bar", headers); + + assertValidHeader("f'", "baz", headers); + assertValidHeader("f`", "baz", headers); + assertValidHeader("f.", "baz", headers); + assertValidHeader("f---", "baz", headers); + assertValidHeader("---", "baz", headers); + assertValidHeader("~~~", "baz", headers); + assertValidHeader("~~~", "b\r\n bar", headers); + assertValidHeader("~~~", "b\r\n\tbar", headers); +} + +function testGetHeader() { + var headers = new nsHttpHeaders(); + + headers.setHeader("Content-Type", "text/html", false); + var c = headers.getHeader("content-type"); + Assert.equal(c, "text/html"); + + headers.setHeader("test", "FOO", false); + c = headers.getHeader("test"); + Assert.equal(c, "FOO"); + + try { + headers.getHeader(":"); + throw new Error("Failed to throw for invalid header"); + } catch (e) { + if (e.result !== Cr.NS_ERROR_INVALID_ARG) { + do_throw("headers.getHeader(':') must throw invalid arg"); + } + } + + try { + headers.getHeader("valid"); + throw new Error("header doesn't exist"); + } catch (e) { + if (e.result !== Cr.NS_ERROR_NOT_AVAILABLE) { + do_throw("shouldn't be a header named 'valid' in headers!"); + } + } +} + +function testHeaderEnumerator() { + var headers = new nsHttpHeaders(); + + var heads = { + foo: "17", + baz: "two six niner", + decaf: "class Program { int .7; int main(){ .7 = 5; return 7 - .7; } }", + }; + + for (var i in heads) { + headers.setHeader(i, heads[i], false); + } + + var en = headers.enumerator; + while (en.hasMoreElements()) { + var it = en.getNext().QueryInterface(Ci.nsISupportsString).data; + Assert.ok(it.toLowerCase() in heads); + delete heads[it.toLowerCase()]; + } + + if (Object.keys(heads).length) { + do_throw("still have properties in heads!?!?"); + } +} + +function testHasHeader() { + var headers = new nsHttpHeaders(); + + headers.setHeader("foo", "bar", false); + Assert.ok(headers.hasHeader("foo")); + Assert.ok(headers.hasHeader("fOo")); + Assert.ok(!headers.hasHeader("not-there")); + + headers.setHeader("f`'~", "bar", false); + Assert.ok(headers.hasHeader("F`'~")); + + try { + headers.hasHeader(":"); + throw new Error("failed to throw"); + } catch (e) { + if (e.result !== Cr.NS_ERROR_INVALID_ARG) { + do_throw(".hasHeader for an invalid name should throw"); + } + } +} diff --git a/netwerk/test/httpserver/test/test_host.js b/netwerk/test/httpserver/test/test_host.js new file mode 100644 index 0000000000..2f5fadde92 --- /dev/null +++ b/netwerk/test/httpserver/test/test_host.js @@ -0,0 +1,608 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/** + * Tests that the scheme, host, and port of the server are correctly recorded + * and used in HTTP requests and responses. + */ + +"use strict"; + +const PORT = 4444; +const FAKE_PORT_ONE = 8888; +const FAKE_PORT_TWO = 8889; + +let srv, id; + +add_task(async function run_test1() { + dump("*** run_test1"); + + srv = createServer(); + + srv.registerPathHandler("/http/1.0-request", http10Request); + srv.registerPathHandler("/http/1.1-good-host", http11goodHost); + srv.registerPathHandler( + "/http/1.1-good-host-wacky-port", + http11goodHostWackyPort + ); + srv.registerPathHandler("/http/1.1-ip-host", http11ipHost); + + srv.start(FAKE_PORT_ONE); + + id = srv.identity; + + // The default location is http://localhost:PORT, where PORT is whatever you + // provided when you started the server. http://127.0.0.1:PORT is also part + // of the default set of locations. + Assert.equal(id.primaryScheme, "http"); + Assert.equal(id.primaryHost, "localhost"); + Assert.equal(id.primaryPort, FAKE_PORT_ONE); + Assert.ok(id.has("http", "localhost", FAKE_PORT_ONE)); + Assert.ok(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + // This should be a nop. + id.add("http", "localhost", FAKE_PORT_ONE); + Assert.equal(id.primaryScheme, "http"); + Assert.equal(id.primaryHost, "localhost"); + Assert.equal(id.primaryPort, FAKE_PORT_ONE); + Assert.ok(id.has("http", "localhost", FAKE_PORT_ONE)); + Assert.ok(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + // Change the primary location and make sure all the getters work correctly. + id.setPrimary("http", "127.0.0.1", FAKE_PORT_ONE); + Assert.equal(id.primaryScheme, "http"); + Assert.equal(id.primaryHost, "127.0.0.1"); + Assert.equal(id.primaryPort, FAKE_PORT_ONE); + Assert.ok(id.has("http", "localhost", FAKE_PORT_ONE)); + Assert.ok(id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + // Okay, now remove the primary location -- we fall back to the original + // location. + id.remove("http", "127.0.0.1", FAKE_PORT_ONE); + Assert.equal(id.primaryScheme, "http"); + Assert.equal(id.primaryHost, "localhost"); + Assert.equal(id.primaryPort, FAKE_PORT_ONE); + Assert.ok(id.has("http", "localhost", FAKE_PORT_ONE)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + // You can't remove every location -- try this and the original default + // location will be silently readded. + id.remove("http", "localhost", FAKE_PORT_ONE); + Assert.equal(id.primaryScheme, "http"); + Assert.equal(id.primaryHost, "localhost"); + Assert.equal(id.primaryPort, FAKE_PORT_ONE); + Assert.ok(id.has("http", "localhost", FAKE_PORT_ONE)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + // Okay, now that we've exercised that behavior, shut down the server and + // restart it on the correct port, to exercise port-changing behaviors at + // server start and stop. + + await new Promise(resolve => srv.stop(resolve)); +}); + +add_task(async function run_test_2() { + dump("*** run_test_2"); + + // Our primary location is gone because it was dependent on the port on which + // the server was running. + checkPrimariesThrow(id); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE)); + + srv.start(FAKE_PORT_TWO); + + // We should have picked up http://localhost:8889 as our primary location now + // that we've restarted. + Assert.equal(id.primaryScheme, "http"); + Assert.equal(id.primaryHost, "localhost"); + Assert.equal(id.primaryPort, FAKE_PORT_TWO); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + Assert.ok(id.has("http", "localhost", FAKE_PORT_TWO)); + Assert.ok(id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + + // Now we're going to see what happens when we shut down with a primary + // location that wasn't a default. That location should persist, and the + // default we remove should still not be present. + id.setPrimary("http", "example.com", FAKE_PORT_TWO); + Assert.ok(id.has("http", "example.com", FAKE_PORT_TWO)); + Assert.ok(id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + Assert.ok(id.has("http", "localhost", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE)); + + id.remove("http", "localhost", FAKE_PORT_TWO); + Assert.ok(id.has("http", "example.com", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_TWO)); + Assert.ok(id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + id.remove("http", "127.0.0.1", FAKE_PORT_TWO); + Assert.ok(id.has("http", "example.com", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + await new Promise(resolve => srv.stop(resolve)); +}); + +add_task(async function run_test_3() { + dump("*** run_test_3"); + + // Only the default added location disappears; any others stay around, + // possibly as the primary location. We may have removed the default primary + // location, but the one we set manually should persist here. + Assert.equal(id.primaryScheme, "http"); + Assert.equal(id.primaryHost, "example.com"); + Assert.equal(id.primaryPort, FAKE_PORT_TWO); + Assert.ok(id.has("http", "example.com", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + + srv.start(PORT); + + // Starting always adds HTTP entries for 127.0.0.1:port and localhost:port. + Assert.ok(id.has("http", "example.com", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + Assert.ok(id.has("http", "localhost", PORT)); + Assert.ok(id.has("http", "127.0.0.1", PORT)); + + // Remove the primary location we'd left set from last time. + id.remove("http", "example.com", FAKE_PORT_TWO); + + // Default-port behavior testing requires the server responds to requests + // claiming to be on one such port. + id.add("http", "localhost", 80); + + // Make sure we don't have anything lying around from running on either the + // first or the second port -- all we should have is our generated default, + // plus the additional port to test "portless" hostport variants. + Assert.ok(id.has("http", "localhost", 80)); + Assert.equal(id.primaryScheme, "http"); + Assert.equal(id.primaryHost, "localhost"); + Assert.equal(id.primaryPort, PORT); + Assert.ok(id.has("http", "localhost", PORT)); + Assert.ok(id.has("http", "127.0.0.1", PORT)); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_ONE)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_ONE)); + Assert.ok(!id.has("http", "example.com", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "localhost", FAKE_PORT_TWO)); + Assert.ok(!id.has("http", "127.0.0.1", FAKE_PORT_TWO)); + + // Okay, finally done with identity testing. Our primary location is the one + // we want it to be, so we're off! + await new Promise(resolve => + runRawTests(tests, resolve, idx => dump(`running test no ${idx}`)) + ); + + // Finally shut down the server. + await new Promise(resolve => srv.stop(resolve)); +}); + +/** ******************* + * UTILITY FUNCTIONS * + *********************/ + +/** + * Verifies that all .primary* getters on a server identity correctly throw + * NS_ERROR_NOT_INITIALIZED. + * + * @param aId : nsIHttpServerIdentity + * the server identity to test + */ +function checkPrimariesThrow(aId) { + let threw = false; + try { + aId.primaryScheme; + } catch (e) { + threw = e.result === Cr.NS_ERROR_NOT_INITIALIZED; + } + Assert.ok(threw); + + threw = false; + try { + aId.primaryHost; + } catch (e) { + threw = e.result === Cr.NS_ERROR_NOT_INITIALIZED; + } + Assert.ok(threw); + + threw = false; + try { + aId.primaryPort; + } catch (e) { + threw = e.result === Cr.NS_ERROR_NOT_INITIALIZED; + } + Assert.ok(threw); +} + +/** + * Utility function to check for a 400 response. + */ +function check400(aData) { + let iter = LineIterator(aData); + + // Status-Line + let { value: firstLine } = iter.next(); + Assert.equal(firstLine.substring(0, HTTP_400_LEADER_LENGTH), HTTP_400_LEADER); +} + +/** ************* + * BEGIN TESTS * + ***************/ + +const HTTP_400_LEADER = "HTTP/1.1 400 "; +const HTTP_400_LEADER_LENGTH = HTTP_400_LEADER.length; + +var test, data; +var tests = []; + +// HTTP/1.0 request, to ensure we see our default scheme/host/port + +function http10Request(request, response) { + writeDetails(request, response); + response.setStatusLine("1.0", 200, "TEST PASSED"); +} +data = "GET /http/1.0-request HTTP/1.0\r\n\r\n"; +function check10(aData) { + let iter = LineIterator(aData); + + // Status-Line + Assert.equal(iter.next().value, "HTTP/1.0 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + let body = [ + "Method: GET", + "Path: /http/1.0-request", + "Query: ", + "Version: 1.0", + "Scheme: http", + "Host: localhost", + "Port: 4444", + ]; + + expectLines(iter, body); +} +test = new RawTest("localhost", PORT, data, check10); +tests.push(test); + +// HTTP/1.1 request, no Host header, expect a 400 response + +// eslint-disable-next-line no-useless-concat +data = "GET /http/1.1-request HTTP/1.1\r\n" + "\r\n"; +test = new RawTest("localhost", PORT, data, check400); +tests.push(test); + +// HTTP/1.1 request, wrong host, expect a 400 response + +data = + // eslint-disable-next-line no-useless-concat + "GET /http/1.1-request HTTP/1.1\r\n" + "Host: not-localhost\r\n" + "\r\n"; +test = new RawTest("localhost", PORT, data, check400); +tests.push(test); + +// HTTP/1.1 request, wrong host/right port, expect a 400 response + +data = + "GET /http/1.1-request HTTP/1.1\r\n" + + "Host: not-localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400); +tests.push(test); + +// HTTP/1.1 request, Host header has host but no port, expect a 400 response + +// eslint-disable-next-line no-useless-concat +data = "GET /http/1.1-request HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "\r\n"; +test = new RawTest("localhost", PORT, data, check400); +tests.push(test); + +// HTTP/1.1 request, Request-URI has wrong port, expect a 400 response + +data = + "GET http://127.0.0.1/http/1.1-request HTTP/1.1\r\n" + + "Host: 127.0.0.1\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400); +tests.push(test); + +// HTTP/1.1 request, Request-URI has wrong port, expect a 400 response + +data = + "GET http://localhost:31337/http/1.1-request HTTP/1.1\r\n" + + "Host: localhost:31337\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400); +tests.push(test); + +// HTTP/1.1 request, Request-URI has wrong scheme, expect a 400 response + +data = + "GET https://localhost:4444/http/1.1-request HTTP/1.1\r\n" + + "Host: localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400); +tests.push(test); + +// HTTP/1.1 request, correct Host header, expect handler's response + +function http11goodHost(request, response) { + writeDetails(request, response); + response.setStatusLine("1.1", 200, "TEST PASSED"); +} +data = + // eslint-disable-next-line no-useless-concat + "GET /http/1.1-good-host HTTP/1.1\r\n" + "Host: localhost:4444\r\n" + "\r\n"; +function check11goodHost(aData) { + let iter = LineIterator(aData); + + // Status-Line + Assert.equal(iter.next().value, "HTTP/1.1 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + let body = [ + "Method: GET", + "Path: /http/1.1-good-host", + "Query: ", + "Version: 1.1", + "Scheme: http", + "Host: localhost", + "Port: 4444", + ]; + + expectLines(iter, body); +} +test = new RawTest("localhost", PORT, data, check11goodHost); +tests.push(test); + +// HTTP/1.1 request, Host header is secondary identity + +function http11ipHost(request, response) { + writeDetails(request, response); + response.setStatusLine("1.1", 200, "TEST PASSED"); +} +data = + // eslint-disable-next-line no-useless-concat + "GET /http/1.1-ip-host HTTP/1.1\r\n" + "Host: 127.0.0.1:4444\r\n" + "\r\n"; +function check11ipHost(aData) { + let iter = LineIterator(aData); + + // Status-Line + Assert.equal(iter.next().value, "HTTP/1.1 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + let body = [ + "Method: GET", + "Path: /http/1.1-ip-host", + "Query: ", + "Version: 1.1", + "Scheme: http", + "Host: 127.0.0.1", + "Port: 4444", + ]; + + expectLines(iter, body); +} +test = new RawTest("localhost", PORT, data, check11ipHost); +tests.push(test); + +// HTTP/1.1 request, absolute path, accurate Host header + +// reusing previous request handler so not defining a new one + +data = + "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" + + "Host: localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHost); +tests.push(test); + +// HTTP/1.1 request, absolute path, inaccurate Host header + +// reusing previous request handler so not defining a new one + +data = + "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" + + "Host: localhost:1234\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHost); +tests.push(test); + +// HTTP/1.1 request, absolute path, different inaccurate Host header + +// reusing previous request handler so not defining a new one + +data = + "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" + + "Host: not-localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHost); +tests.push(test); + +// HTTP/1.1 request, absolute path, yet another inaccurate Host header + +// reusing previous request handler so not defining a new one + +data = + "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" + + "Host: yippity-skippity\r\n" + + "\r\n"; +function checkInaccurate(aData) { + check11goodHost(aData); + + // dynamism setup + srv.identity.setPrimary("http", "127.0.0.1", 4444); +} +test = new RawTest("localhost", PORT, data, checkInaccurate); +tests.push(test); + +// HTTP/1.0 request, absolute path, different inaccurate Host header + +// reusing previous request handler so not defining a new one + +data = + "GET /http/1.0-request HTTP/1.0\r\n" + + "Host: not-localhost:4444\r\n" + + "\r\n"; +function check10ip(aData) { + let iter = LineIterator(aData); + + // Status-Line + Assert.equal(iter.next().value, "HTTP/1.0 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + let body = [ + "Method: GET", + "Path: /http/1.0-request", + "Query: ", + "Version: 1.0", + "Scheme: http", + "Host: 127.0.0.1", + "Port: 4444", + ]; + + expectLines(iter, body); +} +test = new RawTest("localhost", PORT, data, check10ip); +tests.push(test); + +// HTTP/1.1 request, Host header with implied port + +function http11goodHostWackyPort(request, response) { + writeDetails(request, response); + response.setStatusLine("1.1", 200, "TEST PASSED"); +} +data = + "GET /http/1.1-good-host-wacky-port HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; +function check11goodHostWackyPort(aData) { + let iter = LineIterator(aData); + + // Status-Line + Assert.equal(iter.next().value, "HTTP/1.1 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + let body = [ + "Method: GET", + "Path: /http/1.1-good-host-wacky-port", + "Query: ", + "Version: 1.1", + "Scheme: http", + "Host: localhost", + "Port: 80", + ]; + + expectLines(iter, body); +} +test = new RawTest("localhost", PORT, data, check11goodHostWackyPort); +tests.push(test); + +// HTTP/1.1 request, Host header with wacky implied port + +data = + "GET /http/1.1-good-host-wacky-port HTTP/1.1\r\n" + + "Host: localhost:\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHostWackyPort); +tests.push(test); + +// HTTP/1.1 request, absolute URI with implied port + +data = + "GET http://localhost/http/1.1-good-host-wacky-port HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHostWackyPort); +tests.push(test); + +// HTTP/1.1 request, absolute URI with wacky implied port + +data = + "GET http://localhost:/http/1.1-good-host-wacky-port HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHostWackyPort); +tests.push(test); + +// HTTP/1.1 request, absolute URI with explicit implied port, ignored Host + +data = + "GET http://localhost:80/http/1.1-good-host-wacky-port HTTP/1.1\r\n" + + "Host: who-cares\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHostWackyPort); +tests.push(test); + +// HTTP/1.1 request, a malformed Request-URI + +data = + "GET is-this-the-real-life-is-this-just-fantasy HTTP/1.1\r\n" + + "Host: localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400); +tests.push(test); + +// HTTP/1.1 request, a malformed Host header + +// eslint-disable-next-line no-useless-concat +data = "GET /http/1.1-request HTTP/1.1\r\n" + "Host: la la la\r\n" + "\r\n"; +test = new RawTest("localhost", PORT, data, check400); +tests.push(test); + +// HTTP/1.1 request, a malformed Host header but absolute URI, 5.2 sez fine + +data = + "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" + + "Host: la la la\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check11goodHost); +tests.push(test); + +// HTTP/1.0 request, absolute URI, but those aren't valid in HTTP/1.0 + +data = + "GET http://localhost:4444/http/1.1-request HTTP/1.0\r\n" + + "Host: localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400); +tests.push(test); + +// HTTP/1.1 request, absolute URI with unrecognized host + +data = + "GET http://not-localhost:4444/http/1.1-request HTTP/1.1\r\n" + + "Host: not-localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400); +tests.push(test); + +// HTTP/1.1 request, absolute URI with unrecognized host (but not in Host) + +data = + "GET http://not-localhost:4444/http/1.1-request HTTP/1.1\r\n" + + "Host: localhost:4444\r\n" + + "\r\n"; +test = new RawTest("localhost", PORT, data, check400); +tests.push(test); diff --git a/netwerk/test/httpserver/test/test_host_identity.js b/netwerk/test/httpserver/test/test_host_identity.js new file mode 100644 index 0000000000..1a1662d8cf --- /dev/null +++ b/netwerk/test/httpserver/test/test_host_identity.js @@ -0,0 +1,115 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/** + * Tests that the server accepts requests to custom host names. + * This is commonly used in tests that map custom host names to the server via + * a proxy e.g. by XPCShellContentUtils.createHttpServer. + */ + +var srv = createServer(); +srv.start(-1); +registerCleanupFunction(() => new Promise(resolve => srv.stop(resolve))); +const PORT = srv.identity.primaryPort; +srv.registerPathHandler("/dump-request", dumpRequestLines); + +function dumpRequestLines(request, response) { + writeDetails(request, response); + response.setStatusLine(request.httpVersion, 200, "TEST PASSED"); +} + +function makeRawRequest(requestLinePath, hostHeader) { + return `GET ${requestLinePath} HTTP/1.1\r\nHost: ${hostHeader}\r\n\r\n`; +} + +function verifyResponseHostPort(data, query, expectedHost, expectedPort) { + var iter = LineIterator(data); + + // Status-Line + Assert.equal(iter.next().value, "HTTP/1.1 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + var body = [ + "Method: GET", + "Path: /dump-request", + "Query: " + query, + "Version: 1.1", + "Scheme: http", + "Host: " + expectedHost, + "Port: " + expectedPort, + ]; + + expectLines(iter, body); +} + +function runIdentityTest(host, port) { + srv.identity.add("http", host, port); + + function checkAbsoluteRequestURI(data) { + verifyResponseHostPort(data, "absolute", host, port); + } + function checkHostHeader(data) { + verifyResponseHostPort(data, "relative", host, port); + } + + let tests = []; + let test, data; + let hostport = `${host}:${port}`; + data = makeRawRequest(`http://${hostport}/dump-request?absolute`, hostport); + test = new RawTest("localhost", PORT, data, checkAbsoluteRequestURI); + tests.push(test); + + data = makeRawRequest("/dump-request?relative", hostport); + test = new RawTest("localhost", PORT, data, checkHostHeader); + tests.push(test); + return new Promise(resolve => { + runRawTests(tests, resolve); + }); +} + +/** ************* + * BEGIN TESTS * + ***************/ + +add_task(async function test_basic_example_com() { + await runIdentityTest("example.com", 1234); + await runIdentityTest("example.com", 5432); +}); + +add_task(async function test_fully_qualified_domain_name_aka_fqdn() { + await runIdentityTest("fully-qualified-domain-name.", 1234); +}); + +add_task(async function test_ipv4() { + await runIdentityTest("1.2.3.4", 1234); +}); + +add_task(async function test_ipv6() { + Assert.throws( + () => srv.identity.add("http", "[notipv6]", 1234), + /NS_ERROR_ILLEGAL_VALUE/, + "should reject invalid host, clearly not bracketed IPv6" + ); + Assert.throws( + () => srv.identity.add("http", "[::127.0.0.1]", 1234), + /NS_ERROR_ILLEGAL_VALUE/, + "should reject non-canonical IPv6" + ); + await runIdentityTest("[::123]", 1234); + await runIdentityTest("[1:2:3:a:b:c:d:abcd]", 1234); +}); + +add_task(async function test_internationalized_domain_name() { + Assert.throws( + () => srv.identity.add("http", "δοκιμή", 1234), + /NS_ERROR_ILLEGAL_VALUE/, + "should reject IDN not in punycode" + ); + + await runIdentityTest("xn--jxalpdlp", 1234); +}); diff --git a/netwerk/test/httpserver/test/test_linedata.js b/netwerk/test/httpserver/test/test_linedata.js new file mode 100644 index 0000000000..cdffb99956 --- /dev/null +++ b/netwerk/test/httpserver/test/test_linedata.js @@ -0,0 +1,19 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + */ +/* 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/. */ + +// test that the LineData internal data structure works correctly + +function run_test() { + var data = new LineData(); + data.appendBytes(["a".charCodeAt(0), CR]); + + var out = { value: "" }; + Assert.ok(!data.readLine(out)); + + data.appendBytes([LF]); + Assert.ok(data.readLine(out)); + Assert.equal(out.value, "a"); +} diff --git a/netwerk/test/httpserver/test/test_load_module.js b/netwerk/test/httpserver/test/test_load_module.js new file mode 100644 index 0000000000..53718055e1 --- /dev/null +++ b/netwerk/test/httpserver/test/test_load_module.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Ensure httpd.js can be imported as a module and that a server starts. + */ +function run_test() { + const { HttpServer } = ChromeUtils.import( + "resource://testing-common/httpd.js" + ); + + let server = new HttpServer(); + server.start(-1); + + do_test_pending(); + + server.stop(do_test_finished); +} diff --git a/netwerk/test/httpserver/test/test_name_scheme.js b/netwerk/test/httpserver/test/test_name_scheme.js new file mode 100644 index 0000000000..7c50e6ecea --- /dev/null +++ b/netwerk/test/httpserver/test/test_name_scheme.js @@ -0,0 +1,91 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// requests for files ending with a caret (^) are handled specially to enable +// htaccess-like functionality without the need to explicitly disable display +// of such files + +var srv; + +XPCOMUtils.defineLazyGetter(this, "PREFIX", function () { + return "http://localhost:" + srv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test(PREFIX + "/bar.html^", null, start_bar_html_, null), + new Test(PREFIX + "/foo.html^", null, start_foo_html_, null), + new Test(PREFIX + "/normal-file.txt", null, start_normal_file_txt, null), + new Test(PREFIX + "/folder^/file.txt", null, start_folder__file_txt, null), + + new Test(PREFIX + "/foo/bar.html^", null, start_bar_html_, null), + new Test(PREFIX + "/foo/foo.html^", null, start_foo_html_, null), + new Test( + PREFIX + "/foo/normal-file.txt", + null, + start_normal_file_txt, + null + ), + new Test( + PREFIX + "/foo/folder^/file.txt", + null, + start_folder__file_txt, + null + ), + + new Test(PREFIX + "/end-caret^/bar.html^", null, start_bar_html_, null), + new Test(PREFIX + "/end-caret^/foo.html^", null, start_foo_html_, null), + new Test( + PREFIX + "/end-caret^/normal-file.txt", + null, + start_normal_file_txt, + null + ), + new Test( + PREFIX + "/end-caret^/folder^/file.txt", + null, + start_folder__file_txt, + null + ), + ]; +}); + +function run_test() { + srv = createServer(); + + // make sure underscores work in directories "mounted" in directories with + // folders starting with _ + var nameDir = do_get_file("data/name-scheme/"); + srv.registerDirectory("/", nameDir); + srv.registerDirectory("/foo/", nameDir); + srv.registerDirectory("/end-caret^/", nameDir); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +// TEST DATA + +function start_bar_html_(ch) { + Assert.equal(ch.responseStatus, 200); + + Assert.equal(ch.getResponseHeader("Content-Type"), "text/html"); +} + +function start_foo_html_(ch) { + Assert.equal(ch.responseStatus, 404); +} + +function start_normal_file_txt(ch) { + Assert.equal(ch.responseStatus, 200); + Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain"); +} + +function start_folder__file_txt(ch) { + Assert.equal(ch.responseStatus, 200); + Assert.equal(ch.getResponseHeader("Content-Type"), "text/plain"); +} diff --git a/netwerk/test/httpserver/test/test_processasync.js b/netwerk/test/httpserver/test/test_processasync.js new file mode 100644 index 0000000000..d6f85a789d --- /dev/null +++ b/netwerk/test/httpserver/test/test_processasync.js @@ -0,0 +1,272 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* + * Tests for correct behavior of asynchronous responses. + */ + +XPCOMUtils.defineLazyGetter(this, "PREPATH", function () { + return "http://localhost:" + srv.identity.primaryPort; +}); + +var srv; + +function run_test() { + srv = createServer(); + for (var path in handlers) { + srv.registerPathHandler(path, handlers[path]); + } + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +/** ************* + * BEGIN TESTS * + ***************/ + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test(PREPATH + "/handleSync", null, start_handleSync, null), + new Test( + PREPATH + "/handleAsync1", + null, + start_handleAsync1, + stop_handleAsync1 + ), + new Test( + PREPATH + "/handleAsync2", + init_handleAsync2, + start_handleAsync2, + stop_handleAsync2 + ), + new Test( + PREPATH + "/handleAsyncOrdering", + null, + null, + stop_handleAsyncOrdering + ), + ]; +}); + +var handlers = {}; + +function handleSync(request, response) { + response.setStatusLine(request.httpVersion, 500, "handleSync fail"); + + try { + response.finish(); + do_throw("finish called on sync response"); + } catch (e) { + isException(e, Cr.NS_ERROR_UNEXPECTED); + } + + response.setStatusLine(request.httpVersion, 200, "handleSync pass"); +} +handlers["/handleSync"] = handleSync; + +function start_handleSync(ch) { + Assert.equal(ch.responseStatus, 200); + Assert.equal(ch.responseStatusText, "handleSync pass"); +} + +function handleAsync1(request, response) { + response.setStatusLine(request.httpVersion, 500, "Old status line!"); + response.setHeader("X-Foo", "old value", false); + + response.processAsync(); + + response.setStatusLine(request.httpVersion, 200, "New status line!"); + response.setHeader("X-Foo", "new value", false); + + response.finish(); + + try { + response.setStatusLine(request.httpVersion, 500, "Too late!"); + do_throw("late setStatusLine didn't throw"); + } catch (e) { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + try { + response.setHeader("X-Foo", "late value", false); + do_throw("late setHeader didn't throw"); + } catch (e) { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + try { + response.bodyOutputStream; + do_throw("late bodyOutputStream get didn't throw"); + } catch (e) { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + try { + response.write("fugly"); + do_throw("late write() didn't throw"); + } catch (e) { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } +} +handlers["/handleAsync1"] = handleAsync1; + +function start_handleAsync1(ch) { + Assert.equal(ch.responseStatus, 200); + Assert.equal(ch.responseStatusText, "New status line!"); + Assert.equal(ch.getResponseHeader("X-Foo"), "new value"); +} + +function stop_handleAsync1(ch, status, data) { + Assert.equal(data.length, 0); +} + +const startToHeaderDelay = 500; +const startToFinishedDelay = 750; + +function handleAsync2(request, response) { + response.processAsync(); + + response.setStatusLine(request.httpVersion, 200, "Status line"); + response.setHeader("X-Custom-Header", "value", false); + + callLater(startToHeaderDelay, function () { + var preBody = "BO"; + response.bodyOutputStream.write(preBody, preBody.length); + + try { + response.setStatusLine(request.httpVersion, 500, "after body write"); + do_throw("setStatusLine succeeded"); + } catch (e) { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + try { + response.setHeader("X-Custom-Header", "new 1", false); + } catch (e) { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + callLater(startToFinishedDelay - startToHeaderDelay, function () { + var postBody = "DY"; + response.bodyOutputStream.write(postBody, postBody.length); + + response.finish(); + response.finish(); // idempotency + + try { + response.setStatusLine(request.httpVersion, 500, "after finish"); + } catch (e) { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + try { + response.setHeader("X-Custom-Header", "new 2", false); + } catch (e) { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + try { + response.write("EVIL"); + } catch (e) { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + }); + }); +} +handlers["/handleAsync2"] = handleAsync2; + +var startTime_handleAsync2; + +function init_handleAsync2(ch) { + var now = (startTime_handleAsync2 = Date.now()); + dumpn("*** init_HandleAsync2: start time " + now); +} + +function start_handleAsync2(ch) { + var now = Date.now(); + dumpn( + "*** start_handleAsync2: onStartRequest time " + + now + + ", " + + (now - startTime_handleAsync2) + + "ms after start time" + ); + Assert.ok(now >= startTime_handleAsync2 + startToHeaderDelay); + + Assert.equal(ch.responseStatus, 200); + Assert.equal(ch.responseStatusText, "Status line"); + Assert.equal(ch.getResponseHeader("X-Custom-Header"), "value"); +} + +function stop_handleAsync2(ch, status, data) { + var now = Date.now(); + dumpn( + "*** stop_handleAsync2: onStopRequest time " + + now + + ", " + + (now - startTime_handleAsync2) + + "ms after header time" + ); + Assert.ok(now >= startTime_handleAsync2 + startToFinishedDelay); + + Assert.equal(String.fromCharCode.apply(null, data), "BODY"); +} + +/* + * Tests that accessing output stream *before* calling processAsync() works + * correctly, sending written data immediately as it is written, not buffering + * until finish() is called -- which for this much data would mean we would all + * but certainly deadlock, since we're trying to read/write all this data in one + * process on a single thread. + */ +function handleAsyncOrdering(request, response) { + var out = new BinaryOutputStream(response.bodyOutputStream); + + var data = []; + for (var i = 0; i < 65536; i++) { + data[i] = 0; + } + var count = 20; + + var writeData = { + run() { + if (count-- === 0) { + response.finish(); + return; + } + + try { + out.writeByteArray(data); + step(); + } catch (e) { + try { + do_throw("error writing data: " + e); + } finally { + response.finish(); + } + } + }, + }; + function step() { + // Use gThreadManager here because it's expedient, *not* because it's + // intended for public use! If you do this in client code, expect me to + // knowingly break your code by changing the variable name. :-P + gThreadManager.dispatchToMainThread(writeData); + } + step(); + response.processAsync(); +} +handlers["/handleAsyncOrdering"] = handleAsyncOrdering; + +function stop_handleAsyncOrdering(ch, status, data) { + Assert.equal(data.length, 20 * 65536); + data.forEach(function (v, index) { + if (v !== 0) { + do_throw("value " + v + " at index " + index + " should be zero"); + } + }); +} diff --git a/netwerk/test/httpserver/test/test_qi.js b/netwerk/test/httpserver/test/test_qi.js new file mode 100644 index 0000000000..ecd376afc7 --- /dev/null +++ b/netwerk/test/httpserver/test/test_qi.js @@ -0,0 +1,107 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint-disable no-control-regex */ + +/* + * Verify the presence of explicit QueryInterface methods on XPCOM objects + * exposed by httpd.js, rather than allowing QueryInterface to be implicitly + * created by XPConnect. + */ + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test( + "http://localhost:" + srv.identity.primaryPort + "/test", + null, + start_test, + null + ), + new Test( + "http://localhost:" + srv.identity.primaryPort + "/sjs/qi.sjs", + null, + start_sjs_qi, + null + ), + ]; +}); + +var srv; + +function run_test() { + srv = createServer(); + + try { + srv.identity.QueryInterface(Ci.nsIHttpServerIdentity); + } catch (e) { + var exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0]; + do_throw("server identity didn't QI: " + exstr); + return; + } + + srv.registerPathHandler("/test", testHandler); + srv.registerDirectory("/", do_get_file("data/")); + srv.registerContentType("sjs", "sjs"); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +// TEST DATA + +function start_test(ch) { + Assert.equal(ch.responseStatusText, "QI Tests Passed"); + Assert.equal(ch.responseStatus, 200); +} + +function start_sjs_qi(ch) { + Assert.equal(ch.responseStatusText, "SJS QI Tests Passed"); + Assert.equal(ch.responseStatus, 200); +} + +function testHandler(request, response) { + var exstr; + var qid; + + response.setStatusLine(request.httpVersion, 500, "FAIL"); + + var passed = false; + try { + qid = request.QueryInterface(Ci.nsIHttpRequest); + passed = qid === request; + } catch (e) { + exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0]; + response.setStatusLine( + request.httpVersion, + 500, + "request doesn't QI: " + exstr + ); + return; + } + if (!passed) { + response.setStatusLine(request.httpVersion, 500, "request QI'd wrongly?"); + return; + } + + passed = false; + try { + qid = response.QueryInterface(Ci.nsIHttpResponse); + passed = qid === response; + } catch (e) { + exstr = ("" + e).split(/[\x09\x20-\x7f\x81-\xff]+/)[0]; + response.setStatusLine( + request.httpVersion, + 500, + "response doesn't QI: " + exstr + ); + return; + } + if (!passed) { + response.setStatusLine(request.httpVersion, 500, "response QI'd wrongly?"); + return; + } + + response.setStatusLine(request.httpVersion, 200, "QI Tests Passed"); +} diff --git a/netwerk/test/httpserver/test/test_registerdirectory.js b/netwerk/test/httpserver/test/test_registerdirectory.js new file mode 100644 index 0000000000..a4b6413ef1 --- /dev/null +++ b/netwerk/test/httpserver/test/test_registerdirectory.js @@ -0,0 +1,278 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// tests the registerDirectory API + +XPCOMUtils.defineLazyGetter(this, "BASE", function () { + return "http://localhost:" + srv.identity.primaryPort; +}); + +function nocache(ch) { + ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important! +} + +function notFound(ch) { + Assert.equal(ch.responseStatus, 404); + Assert.ok(!ch.requestSucceeded); +} + +function checkOverride(ch) { + Assert.equal(ch.responseStatus, 200); + Assert.equal(ch.responseStatusText, "OK"); + Assert.ok(ch.requestSucceeded); + Assert.equal(ch.getResponseHeader("Override-Succeeded"), "yes"); +} + +function check200(ch) { + Assert.equal(ch.responseStatus, 200); + Assert.equal(ch.responseStatusText, "OK"); +} + +function checkFile(ch, status, data) { + Assert.equal(ch.responseStatus, 200); + Assert.ok(ch.requestSucceeded); + + var actualFile = serverBasePath.clone(); + actualFile.append("test_registerdirectory.js"); + Assert.equal( + ch.getResponseHeader("Content-Length"), + actualFile.fileSize.toString() + ); + Assert.equal( + data.map(v => String.fromCharCode(v)).join(""), + fileContents(actualFile) + ); +} + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + /** ********************* + * without a base path * + ***********************/ + new Test(BASE + "/test_registerdirectory.js", nocache, notFound, null), + + /** ****************** + * with a base path * + ********************/ + new Test( + BASE + "/test_registerdirectory.js", + function (ch) { + nocache(ch); + serverBasePath = testsDirectory.clone(); + srv.registerDirectory("/", serverBasePath); + }, + null, + checkFile + ), + + /** *************************** + * without a base path again * + *****************************/ + new Test( + BASE + "/test_registerdirectory.js", + function (ch) { + nocache(ch); + serverBasePath = null; + srv.registerDirectory("/", serverBasePath); + }, + notFound, + null + ), + + /** ************************* + * registered path handler * + ***************************/ + new Test( + BASE + "/test_registerdirectory.js", + function (ch) { + nocache(ch); + srv.registerPathHandler( + "/test_registerdirectory.js", + override_test_registerdirectory + ); + }, + checkOverride, + null + ), + + /** ********************** + * removed path handler * + ************************/ + new Test( + BASE + "/test_registerdirectory.js", + function init_registerDirectory6(ch) { + nocache(ch); + srv.registerPathHandler("/test_registerdirectory.js", null); + }, + notFound, + null + ), + + /** ****************** + * with a base path * + ********************/ + new Test( + BASE + "/test_registerdirectory.js", + function (ch) { + nocache(ch); + + // set the base path again + serverBasePath = testsDirectory.clone(); + srv.registerDirectory("/", serverBasePath); + }, + null, + checkFile + ), + + /** *********************** + * ...and a path handler * + *************************/ + new Test( + BASE + "/test_registerdirectory.js", + function (ch) { + nocache(ch); + srv.registerPathHandler( + "/test_registerdirectory.js", + override_test_registerdirectory + ); + }, + checkOverride, + null + ), + + /** ********************** + * removed base handler * + ************************/ + new Test( + BASE + "/test_registerdirectory.js", + function (ch) { + nocache(ch); + serverBasePath = null; + srv.registerDirectory("/", serverBasePath); + }, + checkOverride, + null + ), + + /** ********************** + * removed path handler * + ************************/ + new Test( + BASE + "/test_registerdirectory.js", + function (ch) { + nocache(ch); + srv.registerPathHandler("/test_registerdirectory.js", null); + }, + notFound, + null + ), + + /** *********************** + * mapping set up, works * + *************************/ + new Test( + BASE + "/foo/test_registerdirectory.js", + function (ch) { + nocache(ch); + serverBasePath = testsDirectory.clone(); + srv.registerDirectory("/foo/", serverBasePath); + }, + check200, + null + ), + + /** ******************* + * no mapping, fails * + *********************/ + new Test( + BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js", + nocache, + notFound, + null + ), + + /** **************** + * mapping, works * + ******************/ + new Test( + BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js", + function (ch) { + nocache(ch); + srv.registerDirectory( + "/foo/test_registerdirectory.js/", + serverBasePath + ); + }, + null, + checkFile + ), + + /** ********************************** + * two mappings set up, still works * + ************************************/ + new Test(BASE + "/foo/test_registerdirectory.js", nocache, null, checkFile), + + /** ************************ + * remove topmost mapping * + **************************/ + new Test( + BASE + "/foo/test_registerdirectory.js", + function (ch) { + nocache(ch); + srv.registerDirectory("/foo/", null); + }, + notFound, + null + ), + + /** ************************************ + * lower mapping still present, works * + **************************************/ + new Test( + BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js", + nocache, + null, + checkFile + ), + + /** ***************** + * mapping removed * + *******************/ + new Test( + BASE + "/foo/test_registerdirectory.js/test_registerdirectory.js", + function (ch) { + nocache(ch); + srv.registerDirectory("/foo/test_registerdirectory.js/", null); + }, + notFound, + null + ), + ]; +}); + +var srv; +var serverBasePath; +var testsDirectory; + +function run_test() { + testsDirectory = do_get_cwd(); + + srv = createServer(); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +// PATH HANDLERS + +// override of /test_registerdirectory.js +function override_test_registerdirectory(metadata, response) { + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Override-Succeeded", "yes", false); + + var body = "success!"; + response.bodyOutputStream.write(body, body.length); +} diff --git a/netwerk/test/httpserver/test/test_registerfile.js b/netwerk/test/httpserver/test/test_registerfile.js new file mode 100644 index 0000000000..ab2aee531f --- /dev/null +++ b/netwerk/test/httpserver/test/test_registerfile.js @@ -0,0 +1,44 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// tests the registerFile API + +XPCOMUtils.defineLazyGetter(this, "BASE", function () { + return "http://localhost:" + srv.identity.primaryPort; +}); + +var file = do_get_file("test_registerfile.js"); + +function onStart(ch) { + Assert.equal(ch.responseStatus, 200); +} + +function onStop(ch, status, data) { + // not sufficient for equality, but not likely to be wrong! + Assert.equal(data.length, file.fileSize); +} + +XPCOMUtils.defineLazyGetter(this, "test", function () { + return new Test(BASE + "/foo", null, onStart, onStop); +}); + +var srv; + +function run_test() { + srv = createServer(); + + try { + srv.registerFile("/foo", do_get_profile()); + throw new Error("registerFile succeeded!"); + } catch (e) { + isException(e, Cr.NS_ERROR_INVALID_ARG); + } + + srv.registerFile("/foo", file); + srv.start(-1); + + runHttpTests([test], testComplete(srv)); +} diff --git a/netwerk/test/httpserver/test/test_registerprefix.js b/netwerk/test/httpserver/test/test_registerprefix.js new file mode 100644 index 0000000000..2accca4870 --- /dev/null +++ b/netwerk/test/httpserver/test/test_registerprefix.js @@ -0,0 +1,130 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// tests the registerPrefixHandler API + +XPCOMUtils.defineLazyGetter(this, "BASE", function () { + return "http://localhost:" + srv.identity.primaryPort; +}); + +function nocache(ch) { + ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important! +} + +function notFound(ch) { + Assert.equal(ch.responseStatus, 404); + Assert.ok(!ch.requestSucceeded); +} + +function makeCheckOverride(magic) { + return function checkOverride(ch) { + Assert.equal(ch.responseStatus, 200); + Assert.equal(ch.responseStatusText, "OK"); + Assert.ok(ch.requestSucceeded); + Assert.equal(ch.getResponseHeader("Override-Succeeded"), magic); + }; +} + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test( + BASE + "/prefix/dummy", + prefixHandler, + null, + makeCheckOverride("prefix") + ), + new Test( + BASE + "/prefix/dummy", + pathHandler, + null, + makeCheckOverride("path") + ), + new Test( + BASE + "/prefix/subpath/dummy", + longerPrefixHandler, + null, + makeCheckOverride("subpath") + ), + new Test(BASE + "/prefix/dummy", removeHandlers, null, notFound), + new Test( + BASE + "/prefix/subpath/dummy", + newPrefixHandler, + null, + makeCheckOverride("subpath") + ), + ]; +}); + +/** ************************* + * registered prefix handler * + ***************************/ + +function prefixHandler(channel) { + nocache(channel); + srv.registerPrefixHandler("/prefix/", makeOverride("prefix")); +} + +/** ****************************** + * registered path handler on top * + ********************************/ + +function pathHandler(channel) { + nocache(channel); + srv.registerPathHandler("/prefix/dummy", makeOverride("path")); +} + +/** ******************************** + * registered longer prefix handler * + **********************************/ + +function longerPrefixHandler(channel) { + nocache(channel); + srv.registerPrefixHandler("/prefix/subpath/", makeOverride("subpath")); +} + +/** ********************** + * removed prefix handler * + ************************/ + +function removeHandlers(channel) { + nocache(channel); + srv.registerPrefixHandler("/prefix/", null); + srv.registerPathHandler("/prefix/dummy", null); +} + +/** *************************** + * re-register shorter handler * + *****************************/ + +function newPrefixHandler(channel) { + nocache(channel); + srv.registerPrefixHandler("/prefix/", makeOverride("prefix")); +} + +var srv; + +function run_test() { + // Ensure the profile exists. + do_get_profile(); + + srv = createServer(); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +// PATH HANDLERS + +// generate an override +function makeOverride(magic) { + return function override(metadata, response) { + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Override-Succeeded", magic, false); + + var body = "success!"; + response.bodyOutputStream.write(body, body.length); + }; +} diff --git a/netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js b/netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js new file mode 100644 index 0000000000..b1d7a7d071 --- /dev/null +++ b/netwerk/test/httpserver/test/test_request_line_split_in_two_packets.js @@ -0,0 +1,137 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/** + * Tests that even when an incoming request's data for the Request-Line doesn't + * all fit in a single onInputStreamReady notification, the request is handled + * properly. + */ + +var srv = createServer(); +srv.start(-1); +const PORT = srv.identity.primaryPort; + +function run_test() { + srv.registerPathHandler( + "/lots-of-leading-blank-lines", + lotsOfLeadingBlankLines + ); + srv.registerPathHandler("/very-long-request-line", veryLongRequestLine); + + runRawTests(tests, testComplete(srv)); +} + +/** ************* + * BEGIN TESTS * + ***************/ + +var test, gData, str; +var tests = []; + +function veryLongRequestLine(request, response) { + writeDetails(request, response); + response.setStatusLine(request.httpVersion, 200, "TEST PASSED"); +} + +var reallyLong = "0123456789ABCDEF0123456789ABCDEF"; // 32 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 128 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 512 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 2048 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 8192 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 32768 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 131072 +reallyLong = reallyLong + reallyLong + reallyLong + reallyLong; // 524288 +if (reallyLong.length !== 524288) { + throw new TypeError("generated length not as long as expected"); +} +str = + "GET /very-long-request-line?" + + reallyLong + + " HTTP/1.1\r\n" + + "Host: localhost:" + + PORT + + "\r\n" + + "\r\n"; +gData = []; +for (let i = 0; i < str.length; i += 16384) { + gData.push(str.substr(i, 16384)); +} + +function checkVeryLongRequestLine(data) { + var iter = LineIterator(data); + + print("data length: " + data.length); + print("iter object: " + iter); + + // Status-Line + Assert.equal(iter.next().value, "HTTP/1.1 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + var body = [ + "Method: GET", + "Path: /very-long-request-line", + "Query: " + reallyLong, + "Version: 1.1", + "Scheme: http", + "Host: localhost", + "Port: " + PORT, + ]; + + expectLines(iter, body); +} +test = new RawTest("localhost", PORT, gData, checkVeryLongRequestLine); +tests.push(test); + +function lotsOfLeadingBlankLines(request, response) { + writeDetails(request, response); + response.setStatusLine(request.httpVersion, 200, "TEST PASSED"); +} + +var blankLines = "\r\n"; +for (let i = 0; i < 14; i++) { + blankLines += blankLines; +} +str = + blankLines + + "GET /lots-of-leading-blank-lines HTTP/1.1\r\n" + + "Host: localhost:" + + PORT + + "\r\n" + + "\r\n"; +gData = []; +for (let i = 0; i < str.length; i += 100) { + gData.push(str.substr(i, 100)); +} + +function checkLotsOfLeadingBlankLines(data) { + var iter = LineIterator(data); + + // Status-Line + print("data length: " + data.length); + print("iter object: " + iter); + + Assert.equal(iter.next().value, "HTTP/1.1 200 TEST PASSED"); + + skipHeaders(iter); + + // Okay, next line must be the data we expected to be written + var body = [ + "Method: GET", + "Path: /lots-of-leading-blank-lines", + "Query: ", + "Version: 1.1", + "Scheme: http", + "Host: localhost", + "Port: " + PORT, + ]; + + expectLines(iter, body); +} + +test = new RawTest("localhost", PORT, gData, checkLotsOfLeadingBlankLines); +tests.push(test); diff --git a/netwerk/test/httpserver/test/test_response_write.js b/netwerk/test/httpserver/test/test_response_write.js new file mode 100644 index 0000000000..b701c8fa3f --- /dev/null +++ b/netwerk/test/httpserver/test/test_response_write.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// make sure response.write works for strings, and coerces other args to strings + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test( + "http://localhost:" + srv.identity.primaryPort + "/writeString", + null, + check_1234, + succeeded + ), + new Test( + "http://localhost:" + srv.identity.primaryPort + "/writeInt", + null, + check_1234, + succeeded + ), + ]; +}); + +var srv; + +function run_test() { + srv = createServer(); + + srv.registerPathHandler("/writeString", writeString); + srv.registerPathHandler("/writeInt", writeInt); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +// TEST DATA + +function succeeded(ch, status, data) { + Assert.ok(Components.isSuccessCode(status)); + Assert.equal(data.map(v => String.fromCharCode(v)).join(""), "1234"); +} + +function check_1234(ch) { + Assert.equal(ch.getResponseHeader("Content-Length"), "4"); +} + +// PATH HANDLERS + +function writeString(metadata, response) { + response.write("1234"); +} + +function writeInt(metadata, response) { + response.write(1234); +} diff --git a/netwerk/test/httpserver/test/test_seizepower.js b/netwerk/test/httpserver/test/test_seizepower.js new file mode 100644 index 0000000000..5c21a94424 --- /dev/null +++ b/netwerk/test/httpserver/test/test_seizepower.js @@ -0,0 +1,180 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* + * Tests that the seizePower API works correctly. + */ + +XPCOMUtils.defineLazyGetter(this, "PORT", function () { + return srv.identity.primaryPort; +}); + +var srv; + +function run_test() { + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + srv = createServer(); + + srv.registerPathHandler("/raw-data", handleRawData); + srv.registerPathHandler("/called-too-late", handleTooLate); + srv.registerPathHandler("/exceptions", handleExceptions); + srv.registerPathHandler("/async-seizure", handleAsyncSeizure); + srv.registerPathHandler("/seize-after-async", handleSeizeAfterAsync); + + srv.start(-1); + + runRawTests(tests, function () { + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + testComplete(srv)(); + }); +} + +function checkException(fun, err, msg) { + try { + fun(); + } catch (e) { + if (e !== err && e.result !== err) { + do_throw(msg); + } + return; + } + do_throw(msg); +} + +/** *************** + * PATH HANDLERS * + *****************/ + +function handleRawData(request, response) { + response.seizePower(); + response.write("Raw data!"); + response.finish(); +} + +function handleTooLate(request, response) { + response.write("DO NOT WANT"); + var output = response.bodyOutputStream; + + response.seizePower(); + + if (response.bodyOutputStream !== output) { + response.write("bodyOutputStream changed!"); + } else { + response.write("too-late passed"); + } + response.finish(); +} + +function handleExceptions(request, response) { + response.seizePower(); + checkException( + function () { + response.setStatusLine("1.0", 500, "ISE"); + }, + Cr.NS_ERROR_NOT_AVAILABLE, + "setStatusLine should throw not-available after seizePower" + ); + checkException( + function () { + response.setHeader("X-Fail", "FAIL", false); + }, + Cr.NS_ERROR_NOT_AVAILABLE, + "setHeader should throw not-available after seizePower" + ); + checkException( + function () { + response.processAsync(); + }, + Cr.NS_ERROR_NOT_AVAILABLE, + "processAsync should throw not-available after seizePower" + ); + var out = response.bodyOutputStream; + var data = "exceptions test passed"; + out.write(data, data.length); + response.seizePower(); // idempotency test of seizePower + response.finish(); + response.finish(); // idempotency test of finish after seizePower + checkException( + function () { + response.seizePower(); + }, + Cr.NS_ERROR_UNEXPECTED, + "seizePower should throw unexpected after finish" + ); +} + +function handleAsyncSeizure(request, response) { + response.seizePower(); + callLater(1, function () { + response.write("async seizure passed"); + response.bodyOutputStream.close(); + callLater(1, function () { + response.finish(); + }); + }); +} + +function handleSeizeAfterAsync(request, response) { + response.setStatusLine(request.httpVersion, 200, "async seizure pass"); + response.processAsync(); + checkException( + function () { + response.seizePower(); + }, + Cr.NS_ERROR_NOT_AVAILABLE, + "seizePower should throw not-available after processAsync" + ); + callLater(1, function () { + response.finish(); + }); +} + +/** ************* + * BEGIN TESTS * + ***************/ + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new RawTest("localhost", PORT, data0, checkRawData), + new RawTest("localhost", PORT, data1, checkTooLate), + new RawTest("localhost", PORT, data2, checkExceptions), + new RawTest("localhost", PORT, data3, checkAsyncSeizure), + new RawTest("localhost", PORT, data4, checkSeizeAfterAsync), + ]; +}); + +// eslint-disable-next-line no-useless-concat +var data0 = "GET /raw-data HTTP/1.0\r\n" + "\r\n"; +function checkRawData(data) { + Assert.equal(data, "Raw data!"); +} + +// eslint-disable-next-line no-useless-concat +var data1 = "GET /called-too-late HTTP/1.0\r\n" + "\r\n"; +function checkTooLate(data) { + Assert.equal(LineIterator(data).next().value, "too-late passed"); +} + +// eslint-disable-next-line no-useless-concat +var data2 = "GET /exceptions HTTP/1.0\r\n" + "\r\n"; +function checkExceptions(data) { + Assert.equal("exceptions test passed", data); +} + +// eslint-disable-next-line no-useless-concat +var data3 = "GET /async-seizure HTTP/1.0\r\n" + "\r\n"; +function checkAsyncSeizure(data) { + Assert.equal(data, "async seizure passed"); +} + +// eslint-disable-next-line no-useless-concat +var data4 = "GET /seize-after-async HTTP/1.0\r\n" + "\r\n"; +function checkSeizeAfterAsync(data) { + Assert.equal( + LineIterator(data).next().value, + "HTTP/1.0 200 async seizure pass" + ); +} diff --git a/netwerk/test/httpserver/test/test_setindexhandler.js b/netwerk/test/httpserver/test/test_setindexhandler.js new file mode 100644 index 0000000000..8483fb1a5f --- /dev/null +++ b/netwerk/test/httpserver/test/test_setindexhandler.js @@ -0,0 +1,60 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// Make sure setIndexHandler works as expected + +var srv, serverBasePath; + +function run_test() { + srv = createServer(); + serverBasePath = do_get_profile(); + srv.registerDirectory("/", serverBasePath); + srv.setIndexHandler(myIndexHandler); + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + srv.identity.primaryPort + "/"; +}); + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test(URL, init, startCustomIndexHandler, stopCustomIndexHandler), + new Test(URL, init, startDefaultIndexHandler, stopDefaultIndexHandler), + ]; +}); + +function init(ch) { + ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // important! +} +function startCustomIndexHandler(ch) { + Assert.equal(ch.getResponseHeader("Content-Length"), "10"); + srv.setIndexHandler(null); +} +function stopCustomIndexHandler(ch, status, data) { + Assert.ok(Components.isSuccessCode(status)); + Assert.equal(String.fromCharCode.apply(null, data), "directory!"); +} + +function startDefaultIndexHandler(ch) { + Assert.equal(ch.responseStatus, 200); +} +function stopDefaultIndexHandler(ch, status, data) { + Assert.ok(Components.isSuccessCode(status)); +} + +// PATH HANDLERS + +function myIndexHandler(metadata, response) { + var dir = metadata.getProperty("directory"); + Assert.ok(dir != null); + Assert.ok(dir instanceof Ci.nsIFile); + Assert.ok(dir.equals(serverBasePath)); + + response.write("directory!"); +} diff --git a/netwerk/test/httpserver/test/test_setstatusline.js b/netwerk/test/httpserver/test/test_setstatusline.js new file mode 100644 index 0000000000..f27e2b97bb --- /dev/null +++ b/netwerk/test/httpserver/test/test_setstatusline.js @@ -0,0 +1,142 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// exercise nsIHttpResponse.setStatusLine, ensure its atomicity, and ensure the +// specified behavior occurs if it's not called + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + srv.identity.primaryPort; +}); + +var srv; + +function run_test() { + srv = createServer(); + + srv.registerPathHandler("/no/setstatusline", noSetstatusline); + srv.registerPathHandler("/http1_0", http1_0); + srv.registerPathHandler("/http1_1", http1_1); + srv.registerPathHandler("/invalidVersion", invalidVersion); + srv.registerPathHandler("/invalidStatus", invalidStatus); + srv.registerPathHandler("/invalidDescription", invalidDescription); + srv.registerPathHandler("/crazyCode", crazyCode); + srv.registerPathHandler("/nullVersion", nullVersion); + + srv.start(-1); + + runHttpTests(tests, testComplete(srv)); +} + +/** *********** + * UTILITIES * + *************/ + +function checkStatusLine( + channel, + httpMaxVer, + httpMinVer, + httpCode, + statusText +) { + Assert.equal(channel.responseStatus, httpCode); + Assert.equal(channel.responseStatusText, statusText); + + var respMaj = {}, + respMin = {}; + channel.getResponseVersion(respMaj, respMin); + Assert.equal(respMaj.value, httpMaxVer); + Assert.equal(respMin.value, httpMinVer); +} + +/** ******* + * TESTS * + *********/ + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test(URL + "/no/setstatusline", null, startNoSetStatusLine, stop), + new Test(URL + "/http1_0", null, startHttp1_0, stop), + new Test(URL + "/http1_1", null, startHttp1_1, stop), + new Test(URL + "/invalidVersion", null, startPassedTrue, stop), + new Test(URL + "/invalidStatus", null, startPassedTrue, stop), + new Test(URL + "/invalidDescription", null, startPassedTrue, stop), + new Test(URL + "/crazyCode", null, startCrazy, stop), + new Test(URL + "/nullVersion", null, startNullVersion, stop), + ]; +}); + +// /no/setstatusline +function noSetstatusline(metadata, response) {} +function startNoSetStatusLine(ch) { + checkStatusLine(ch, 1, 1, 200, "OK"); +} +function stop(ch, status, data) { + Assert.ok(Components.isSuccessCode(status)); +} + +// /http1_0 +function http1_0(metadata, response) { + response.setStatusLine("1.0", 200, "OK"); +} +function startHttp1_0(ch) { + checkStatusLine(ch, 1, 0, 200, "OK"); +} + +// /http1_1 +function http1_1(metadata, response) { + response.setStatusLine("1.1", 200, "OK"); +} +function startHttp1_1(ch) { + checkStatusLine(ch, 1, 1, 200, "OK"); +} + +// /invalidVersion +function invalidVersion(metadata, response) { + try { + response.setStatusLine(" 1.0", 200, "FAILED"); + } catch (e) { + response.setHeader("Passed", "true", false); + } +} +function startPassedTrue(ch) { + checkStatusLine(ch, 1, 1, 200, "OK"); + Assert.equal(ch.getResponseHeader("Passed"), "true"); +} + +// /invalidStatus +function invalidStatus(metadata, response) { + try { + response.setStatusLine("1.0", 1000, "FAILED"); + } catch (e) { + response.setHeader("Passed", "true", false); + } +} + +// /invalidDescription +function invalidDescription(metadata, response) { + try { + response.setStatusLine("1.0", 200, "FAILED\x01"); + } catch (e) { + response.setHeader("Passed", "true", false); + } +} + +// /crazyCode +function crazyCode(metadata, response) { + response.setStatusLine("1.1", 617, "Crazy"); +} +function startCrazy(ch) { + checkStatusLine(ch, 1, 1, 617, "Crazy"); +} + +// /nullVersion +function nullVersion(metadata, response) { + response.setStatusLine(null, 255, "NULL"); +} +function startNullVersion(ch) { + // currently, this server implementation defaults to 1.1 + checkStatusLine(ch, 1, 1, 255, "NULL"); +} diff --git a/netwerk/test/httpserver/test/test_sjs.js b/netwerk/test/httpserver/test/test_sjs.js new file mode 100644 index 0000000000..51d92ec4e0 --- /dev/null +++ b/netwerk/test/httpserver/test/test_sjs.js @@ -0,0 +1,243 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// tests support for server JS-generated pages + +var srv = createServer(); + +var sjs = do_get_file("data/sjs/cgi.sjs"); +// NB: The server has no state at this point -- all state is set up and torn +// down in the tests, because we run the same tests twice with only a +// different query string on the requests, followed by the oddball +// test that doesn't care about throwing or not. +srv.start(-1); +const PORT = srv.identity.primaryPort; + +const BASE = "http://localhost:" + PORT; +var test; +var tests = []; + +/** ******************* + * UTILITY FUNCTIONS * + *********************/ + +function bytesToString(bytes) { + return bytes + .map(function (v) { + return String.fromCharCode(v); + }) + .join(""); +} + +function skipCache(ch) { + ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; +} + +/** ****************** + * DEFINE THE TESTS * + ********************/ + +/** + * Adds the set of tests defined in here, differentiating between tests with a + * SJS which throws an exception and creates a server error and tests with a + * normal, successful SJS. + */ +function setupTests(throwing) { + const TEST_URL = BASE + "/cgi.sjs" + (throwing ? "?throw" : ""); + + // registerFile with SJS => raw text + + function setupFile(ch) { + srv.registerFile("/cgi.sjs", sjs); + skipCache(ch); + } + + function verifyRawText(channel, status, bytes) { + dumpn(channel.originalURI.spec); + Assert.equal(bytesToString(bytes), fileContents(sjs)); + } + + test = new Test(TEST_URL, setupFile, null, verifyRawText); + tests.push(test); + + // add mapping, => interpreted + + function addTypeMapping(ch) { + srv.registerContentType("sjs", "sjs"); + skipCache(ch); + } + + function checkType(ch) { + if (throwing) { + Assert.ok(!ch.requestSucceeded); + Assert.equal(ch.responseStatus, 500); + } else { + Assert.equal(ch.contentType, "text/plain"); + } + } + + function checkContents(ch, status, data) { + if (!throwing) { + Assert.equal("PASS", bytesToString(data)); + } + } + + test = new Test(TEST_URL, addTypeMapping, checkType, checkContents); + tests.push(test); + + // remove file/type mapping, map containing directory => raw text + + function setupDirectoryAndRemoveType(ch) { + dumpn("removing type mapping"); + srv.registerContentType("sjs", null); + srv.registerFile("/cgi.sjs", null); + srv.registerDirectory("/", sjs.parent); + skipCache(ch); + } + + test = new Test(TEST_URL, setupDirectoryAndRemoveType, null, verifyRawText); + tests.push(test); + + // add mapping, => interpreted + + function contentAndCleanup(ch, status, data) { + checkContents(ch, status, data); + + // clean up state we've set up + srv.registerDirectory("/", null); + srv.registerContentType("sjs", null); + } + + test = new Test(TEST_URL, addTypeMapping, checkType, contentAndCleanup); + tests.push(test); + + // NB: No remaining state in the server right now! If we have any here, + // either the second run of tests (without ?throw) or the tests added + // after the two sets will almost certainly fail. +} + +/** *************** + * ADD THE TESTS * + *****************/ + +setupTests(true); +setupTests(false); + +// Test that when extension-mappings are used, the entire filename cannot be +// treated as an extension -- there must be at least one dot for a filename to +// match an extension. + +function init(ch) { + // clean up state we've set up + srv.registerDirectory("/", sjs.parent); + srv.registerContentType("sjs", "sjs"); + skipCache(ch); +} + +function checkNotSJS(ch, status, data) { + Assert.notEqual("FAIL", bytesToString(data)); +} + +test = new Test(BASE + "/sjs", init, null, checkNotSJS); +tests.push(test); + +// Test that Range requests are passed through to the SJS file without +// bounds checking. + +function rangeInit(expectedRangeHeader) { + return function setupRangeRequest(ch) { + ch.setRequestHeader("Range", expectedRangeHeader, false); + }; +} + +function checkRangeResult(ch) { + try { + var val = ch.getResponseHeader("Content-Range"); + } catch (e) { + /* IDL doesn't specify a particular exception to require */ + } + if (val !== undefined) { + do_throw( + "should not have gotten a Content-Range header, but got one " + + "with this value: " + + val + ); + } + Assert.equal(200, ch.responseStatus); + Assert.equal("OK", ch.responseStatusText); +} + +test = new Test( + BASE + "/range-checker.sjs", + rangeInit("not-a-bytes-equals-specifier"), + checkRangeResult, + null +); +tests.push(test); +test = new Test( + BASE + "/range-checker.sjs", + rangeInit("bytes=-"), + checkRangeResult, + null +); +tests.push(test); +test = new Test( + BASE + "/range-checker.sjs", + rangeInit("bytes=1000000-"), + checkRangeResult, + null +); +tests.push(test); +test = new Test( + BASE + "/range-checker.sjs", + rangeInit("bytes=1-4"), + checkRangeResult, + null +); +tests.push(test); +test = new Test( + BASE + "/range-checker.sjs", + rangeInit("bytes=-4"), + checkRangeResult, + null +); +tests.push(test); + +// One last test: for file mappings, the content-type is determined by the +// extension of the file on the server, not by the extension of the requested +// path. + +function setupFileMapping(ch) { + srv.registerFile("/script.html", sjs); +} + +function onStart(ch) { + Assert.equal(ch.contentType, "text/plain"); +} + +function onStop(ch, status, data) { + Assert.equal("PASS", bytesToString(data)); +} + +test = new Test(BASE + "/script.html", setupFileMapping, onStart, onStop); +tests.push(test); + +/** *************** + * RUN THE TESTS * + *****************/ + +function run_test() { + // Test for a content-type which isn't a field-value + try { + srv.registerContentType("foo", "bar\nbaz"); + throw new Error( + "this server throws on content-types which aren't field-values" + ); + } catch (e) { + isException(e, Cr.NS_ERROR_INVALID_ARG); + } + runHttpTests(tests, testComplete(srv)); +} diff --git a/netwerk/test/httpserver/test/test_sjs_object_state.js b/netwerk/test/httpserver/test/test_sjs_object_state.js new file mode 100644 index 0000000000..4362dc7641 --- /dev/null +++ b/netwerk/test/httpserver/test/test_sjs_object_state.js @@ -0,0 +1,305 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* + * Tests that the object-state-preservation mechanism works correctly. + */ + +XPCOMUtils.defineLazyGetter(this, "PATH", function () { + return "http://localhost:" + srv.identity.primaryPort + "/object-state.sjs"; +}); + +var srv; + +function run_test() { + srv = createServer(); + var sjsDir = do_get_file("data/sjs/"); + srv.registerDirectory("/", sjsDir); + srv.registerContentType("sjs", "sjs"); + srv.start(-1); + + do_test_pending(); + + new HTTPTestLoader(PATH + "?state=initial", initialStart, initialStop); +} + +/** ****************** + * OBSERVER METHODS * + ********************/ + +/* + * In practice the current implementation will guarantee an exact ordering of + * all start and stop callbacks. However, in the interests of robustness, this + * test will pass given any valid ordering of callbacks. Any ordering of calls + * which obeys the partial ordering shown by this directed acyclic graph will be + * handled correctly: + * + * initialStart + * | + * V + * intermediateStart + * | + * V + * intermediateStop + * | \ + * | V + * | initialStop + * V + * triggerStart + * | + * V + * triggerStop + * + */ + +var initialStarted = false; +function initialStart(ch) { + dumpn("*** initialStart"); + + if (initialStarted) { + do_throw("initialStart: initialStarted is true?!?!"); + } + + initialStarted = true; + + new HTTPTestLoader( + PATH + "?state=intermediate", + intermediateStart, + intermediateStop + ); +} + +var initialStopped = false; +function initialStop(ch, status, data) { + dumpn("*** initialStop"); + + Assert.equal( + data + .map(function (v) { + return String.fromCharCode(v); + }) + .join(""), + "done" + ); + + Assert.equal(srv.getObjectState("object-state-test"), null); + + if (!initialStarted) { + do_throw("initialStop: initialStarted is false?!?!"); + } + if (initialStopped) { + do_throw("initialStop: initialStopped is true?!?!"); + } + if (!intermediateStarted) { + do_throw("initialStop: intermediateStarted is false?!?!"); + } + if (!intermediateStopped) { + do_throw("initialStop: intermediateStopped is false?!?!"); + } + + initialStopped = true; + + checkForFinish(); +} + +var intermediateStarted = false; +function intermediateStart(ch) { + dumpn("*** intermediateStart"); + + Assert.notEqual(srv.getObjectState("object-state-test"), null); + + if (!initialStarted) { + do_throw("intermediateStart: initialStarted is false?!?!"); + } + if (intermediateStarted) { + do_throw("intermediateStart: intermediateStarted is true?!?!"); + } + + intermediateStarted = true; +} + +var intermediateStopped = false; +function intermediateStop(ch, status, data) { + dumpn("*** intermediateStop"); + + Assert.equal( + data + .map(function (v) { + return String.fromCharCode(v); + }) + .join(""), + "intermediate" + ); + + Assert.notEqual(srv.getObjectState("object-state-test"), null); + + if (!initialStarted) { + do_throw("intermediateStop: initialStarted is false?!?!"); + } + if (!intermediateStarted) { + do_throw("intermediateStop: intermediateStarted is false?!?!"); + } + if (intermediateStopped) { + do_throw("intermediateStop: intermediateStopped is true?!?!"); + } + + intermediateStopped = true; + + new HTTPTestLoader(PATH + "?state=trigger", triggerStart, triggerStop); +} + +var triggerStarted = false; +function triggerStart(ch) { + dumpn("*** triggerStart"); + + if (!initialStarted) { + do_throw("triggerStart: initialStarted is false?!?!"); + } + if (!intermediateStarted) { + do_throw("triggerStart: intermediateStarted is false?!?!"); + } + if (!intermediateStopped) { + do_throw("triggerStart: intermediateStopped is false?!?!"); + } + if (triggerStarted) { + do_throw("triggerStart: triggerStarted is true?!?!"); + } + + triggerStarted = true; +} + +var triggerStopped = false; +function triggerStop(ch, status, data) { + dumpn("*** triggerStop"); + + Assert.equal( + data + .map(function (v) { + return String.fromCharCode(v); + }) + .join(""), + "trigger" + ); + + if (!initialStarted) { + do_throw("triggerStop: initialStarted is false?!?!"); + } + if (!intermediateStarted) { + do_throw("triggerStop: intermediateStarted is false?!?!"); + } + if (!intermediateStopped) { + do_throw("triggerStop: intermediateStopped is false?!?!"); + } + if (!triggerStarted) { + do_throw("triggerStop: triggerStarted is false?!?!"); + } + if (triggerStopped) { + do_throw("triggerStop: triggerStopped is false?!?!"); + } + + triggerStopped = true; + + checkForFinish(); +} + +var finished = false; +function checkForFinish() { + if (finished) { + try { + do_throw("uh-oh, how are we being finished twice?!?!"); + } finally { + // eslint-disable-next-line no-undef + quit(1); + } + } + + if (triggerStopped && initialStopped) { + finished = true; + try { + Assert.equal(srv.getObjectState("object-state-test"), null); + + if (!initialStarted) { + do_throw("checkForFinish: initialStarted is false?!?!"); + } + if (!intermediateStarted) { + do_throw("checkForFinish: intermediateStarted is false?!?!"); + } + if (!intermediateStopped) { + do_throw("checkForFinish: intermediateStopped is false?!?!"); + } + if (!triggerStarted) { + do_throw("checkForFinish: triggerStarted is false?!?!"); + } + } finally { + srv.stop(do_test_finished); + } + } +} + +/** ******************************* + * UTILITY OBSERVABLE URL LOADER * + *********************************/ + +/** Stream listener for the channels. */ +function HTTPTestLoader(path, start, stop) { + /** Path to load. */ + this._path = path; + + /** Array of bytes of data in body of response. */ + this._data = []; + + /** onStartRequest callback. */ + this._start = start; + + /** onStopRequest callback. */ + this._stop = stop; + + var channel = makeChannel(path); + channel.asyncOpen(this); +} +HTTPTestLoader.prototype = { + onStartRequest(request) { + dumpn("*** HTTPTestLoader.onStartRequest for " + this._path); + + var ch = request + .QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + try { + try { + this._start(ch); + } catch (e) { + do_throw(this._path + ": error in onStartRequest: " + e); + } + } catch (e) { + dumpn( + "!!! swallowing onStartRequest exception so onStopRequest is " + + "called..." + ); + } + }, + onDataAvailable(request, inputStream, offset, count) { + dumpn("*** HTTPTestLoader.onDataAvailable for " + this._path); + + Array.prototype.push.apply( + this._data, + makeBIS(inputStream).readByteArray(count) + ); + }, + onStopRequest(request, status) { + dumpn("*** HTTPTestLoader.onStopRequest for " + this._path); + + var ch = request + .QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + this._stop(ch, status, this._data); + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), +}; diff --git a/netwerk/test/httpserver/test/test_sjs_state.js b/netwerk/test/httpserver/test/test_sjs_state.js new file mode 100644 index 0000000000..d45e750aea --- /dev/null +++ b/netwerk/test/httpserver/test/test_sjs_state.js @@ -0,0 +1,203 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// exercises the server's state-preservation API + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + srv.identity.primaryPort; +}); + +var srv; + +function run_test() { + srv = createServer(); + var sjsDir = do_get_file("data/sjs/"); + srv.registerDirectory("/", sjsDir); + srv.registerContentType("sjs", "sjs"); + srv.registerPathHandler("/path-handler", pathHandler); + srv.start(-1); + + function done() { + Assert.equal(srv.getSharedState("shared-value"), "done!"); + Assert.equal( + srv.getState("/path-handler", "private-value"), + "pathHandlerPrivate2" + ); + Assert.equal(srv.getState("/state1.sjs", "private-value"), ""); + Assert.equal(srv.getState("/state2.sjs", "private-value"), "newPrivate5"); + do_test_pending(); + srv.stop(function () { + do_test_finished(); + }); + } + + runHttpTests(tests, done); +} + +/** ********** + * HANDLERS * + ************/ + +var firstTime = true; + +function pathHandler(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + response.setHeader( + "X-Old-Shared-Value", + srv.getSharedState("shared-value"), + false + ); + response.setHeader( + "X-Old-Private-Value", + srv.getState("/path-handler", "private-value"), + false + ); + + var privateValue, sharedValue; + if (firstTime) { + firstTime = false; + privateValue = "pathHandlerPrivate"; + sharedValue = "pathHandlerShared"; + } else { + privateValue = "pathHandlerPrivate2"; + sharedValue = ""; + } + + srv.setState("/path-handler", "private-value", privateValue); + srv.setSharedState("shared-value", sharedValue); + + response.setHeader("X-New-Private-Value", privateValue, false); + response.setHeader("X-New-Shared-Value", sharedValue, false); +} + +/** ************* + * BEGIN TESTS * + ***************/ + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + return [ + new Test( + // eslint-disable-next-line no-useless-concat + URL + "/state1.sjs?" + "newShared=newShared&newPrivate=newPrivate", + null, + start_initial, + null + ), + new Test( + // eslint-disable-next-line no-useless-concat + URL + "/state1.sjs?" + "newShared=newShared2&newPrivate=newPrivate2", + null, + start_overwrite, + null + ), + new Test( + // eslint-disable-next-line no-useless-concat + URL + "/state1.sjs?" + "newShared=&newPrivate=newPrivate3", + null, + start_remove, + null + ), + new Test(URL + "/path-handler", null, start_handler, null), + new Test(URL + "/path-handler", null, start_handler_again, null), + new Test( + // eslint-disable-next-line no-useless-concat + URL + "/state2.sjs?" + "newShared=newShared4&newPrivate=newPrivate4", + null, + start_other_initial, + null + ), + new Test( + // eslint-disable-next-line no-useless-concat + URL + "/state2.sjs?" + "newShared=", + null, + start_other_remove_ignore, + null + ), + new Test( + // eslint-disable-next-line no-useless-concat + URL + "/state2.sjs?" + "newShared=newShared5&newPrivate=newPrivate5", + null, + start_other_set_new, + null + ), + new Test( + // eslint-disable-next-line no-useless-concat + URL + "/state1.sjs?" + "newShared=done!&newPrivate=", + null, + start_set_remove_original, + null + ), + ]; +}); + +/* Hack around bug 474845 for now. */ +function getHeaderFunction(ch) { + function getHeader(name) { + try { + return ch.getResponseHeader(name); + } catch (e) { + if (e.result !== Cr.NS_ERROR_NOT_AVAILABLE) { + throw e; + } + } + return ""; + } + return getHeader; +} + +function expectValues(ch, oldShared, newShared, oldPrivate, newPrivate) { + var getHeader = getHeaderFunction(ch); + + Assert.equal(ch.responseStatus, 200); + Assert.equal(getHeader("X-Old-Shared-Value"), oldShared); + Assert.equal(getHeader("X-New-Shared-Value"), newShared); + Assert.equal(getHeader("X-Old-Private-Value"), oldPrivate); + Assert.equal(getHeader("X-New-Private-Value"), newPrivate); +} + +function start_initial(ch) { + dumpn("XXX start_initial"); + expectValues(ch, "", "newShared", "", "newPrivate"); +} + +function start_overwrite(ch) { + expectValues(ch, "newShared", "newShared2", "newPrivate", "newPrivate2"); +} + +function start_remove(ch) { + expectValues(ch, "newShared2", "", "newPrivate2", "newPrivate3"); +} + +function start_handler(ch) { + expectValues(ch, "", "pathHandlerShared", "", "pathHandlerPrivate"); +} + +function start_handler_again(ch) { + expectValues( + ch, + "pathHandlerShared", + "", + "pathHandlerPrivate", + "pathHandlerPrivate2" + ); +} + +function start_other_initial(ch) { + expectValues(ch, "", "newShared4", "", "newPrivate4"); +} + +function start_other_remove_ignore(ch) { + expectValues(ch, "newShared4", "", "newPrivate4", ""); +} + +function start_other_set_new(ch) { + expectValues(ch, "", "newShared5", "newPrivate4", "newPrivate5"); +} + +function start_set_remove_original(ch) { + expectValues(ch, "newShared5", "done!", "newPrivate3", ""); +} diff --git a/netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js b/netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js new file mode 100644 index 0000000000..2f8b73fe52 --- /dev/null +++ b/netwerk/test/httpserver/test/test_sjs_throwing_exceptions.js @@ -0,0 +1,73 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* + * Tests that running an SJS a whole lot of times doesn't have any ill effects + * (like exceeding open-file limits due to not closing the SJS file each time, + * then preventing any file from being opened). + */ + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + srv.identity.primaryPort; +}); + +var srv; + +function run_test() { + srv = createServer(); + var sjsDir = do_get_file("data/sjs/"); + srv.registerDirectory("/", sjsDir); + srv.registerContentType("sjs", "sjs"); + srv.start(-1); + + function done() { + do_test_pending(); + srv.stop(function () { + do_test_finished(); + }); + Assert.equal(gStartCount, TEST_RUNS); + Assert.ok(lastPassed); + } + + runHttpTests(tests, done); +} + +/** ************* + * BEGIN TESTS * + ***************/ + +var gStartCount = 0; +var lastPassed = false; + +// This hits the open-file limit for me on OS X; your mileage may vary. +const TEST_RUNS = 250; + +XPCOMUtils.defineLazyGetter(this, "tests", function () { + var _tests = new Array(TEST_RUNS + 1); + var _test = new Test(URL + "/thrower.sjs?throw", null, start_thrower); + for (var i = 0; i < TEST_RUNS; i++) { + _tests[i] = _test; + } + // ...and don't forget to stop! + _tests[TEST_RUNS] = new Test(URL + "/thrower.sjs", null, start_last); + return _tests; +}); + +function start_thrower(ch) { + Assert.equal(ch.responseStatus, 500); + Assert.ok(!ch.requestSucceeded); + + gStartCount++; +} + +function start_last(ch) { + Assert.equal(ch.responseStatus, 200); + Assert.ok(ch.requestSucceeded); + + Assert.equal(ch.getResponseHeader("X-Test-Status"), "PASS"); + + lastPassed = true; +} diff --git a/netwerk/test/httpserver/test/test_start_stop.js b/netwerk/test/httpserver/test/test_start_stop.js new file mode 100644 index 0000000000..66b801f4ac --- /dev/null +++ b/netwerk/test/httpserver/test/test_start_stop.js @@ -0,0 +1,166 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* + * Tests for correct behavior of the server start() and stop() methods. + */ + +XPCOMUtils.defineLazyGetter(this, "PORT", function () { + return srv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "PREPATH", function () { + return "http://localhost:" + PORT; +}); + +var srv, srv2; + +function run_test() { + if (mozinfo.os == "win") { + dumpn( + "*** not running test_start_stop.js on Windows for now, because " + + "Windows is dumb" + ); + return; + } + + dumpn("*** run_test"); + + srv = createServer(); + srv.start(-1); + + try { + srv.start(PORT); + do_throw("starting a started server"); + } catch (e) { + isException(e, Cr.NS_ERROR_ALREADY_INITIALIZED); + } + + do_test_pending(); + srv.stop(function () { + try { + do_test_pending(); + run_test_2(); + } finally { + do_test_finished(); + } + }); +} + +function run_test_2() { + dumpn("*** run_test_2"); + + do_test_finished(); + + srv.start(PORT); + srv2 = createServer(); + + try { + srv2.start(PORT); + do_throw("two servers on one port?"); + } catch (e) { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + do_test_pending(); + try { + srv.stop({ + onStopped() { + try { + do_test_pending(); + run_test_3(); + } finally { + do_test_finished(); + } + }, + }); + } catch (e) { + do_throw("error stopping with an object: " + e); + } +} + +function run_test_3() { + dumpn("*** run_test_3"); + + do_test_finished(); + + srv.start(PORT); + + do_test_pending(); + try { + srv.stop().then(function () { + try { + do_test_pending(); + run_test_4(); + } finally { + do_test_finished(); + } + }); + } catch (e) { + do_throw("error stopping with an object: " + e); + } +} + +function run_test_4() { + dumpn("*** run_test_4"); + + do_test_finished(); + + srv.registerPathHandler("/handle", handle); + srv.start(PORT); + + // Don't rely on the exact (but implementation-constant) sequence of events + // as it currently exists by making either run_test_5 or serverStopped handle + // the final shutdown. + do_test_pending(); + + runHttpTests([new Test(PREPATH + "/handle")], run_test_5); +} + +var testsComplete = false; + +function run_test_5() { + dumpn("*** run_test_5"); + + testsComplete = true; + if (stopped) { + do_test_finished(); + } +} + +const INTERVAL = 500; + +function handle(request, response) { + response.processAsync(); + + dumpn("*** stopping server..."); + srv.stop(serverStopped); + + callLater(INTERVAL, function () { + Assert.ok(!stopped); + + callLater(INTERVAL, function () { + Assert.ok(!stopped); + response.finish(); + + try { + response.processAsync(); + do_throw("late processAsync didn't throw?"); + } catch (e) { + isException(e, Cr.NS_ERROR_UNEXPECTED); + } + }); + }); +} + +var stopped = false; +function serverStopped() { + dumpn("*** server really, fully shut down now"); + stopped = true; + if (testsComplete) { + do_test_finished(); + } +} diff --git a/netwerk/test/httpserver/test/test_start_stop_ipv6.js b/netwerk/test/httpserver/test/test_start_stop_ipv6.js new file mode 100644 index 0000000000..6c4b4d99a8 --- /dev/null +++ b/netwerk/test/httpserver/test/test_start_stop_ipv6.js @@ -0,0 +1,166 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* + * Tests for correct behavior of the server start_ipv6() and stop() methods. + */ + +XPCOMUtils.defineLazyGetter(this, "PORT", function () { + return srv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "PREPATH", function () { + return "http://localhost:" + PORT; +}); + +var srv, srv2; + +function run_test() { + if (mozinfo.os == "win") { + dumpn( + "*** not running test_start_stop.js on Windows for now, because " + + "Windows is dumb" + ); + return; + } + + dumpn("*** run_test"); + + srv = createServer(); + srv.start_ipv6(-1); + + try { + srv.start_ipv6(PORT); + do_throw("starting a started server"); + } catch (e) { + isException(e, Cr.NS_ERROR_ALREADY_INITIALIZED); + } + + do_test_pending(); + srv.stop(function () { + try { + do_test_pending(); + run_test_2(); + } finally { + do_test_finished(); + } + }); +} + +function run_test_2() { + dumpn("*** run_test_2"); + + do_test_finished(); + + srv.start_ipv6(PORT); + srv2 = createServer(); + + try { + srv2.start_ipv6(PORT); + do_throw("two servers on one port?"); + } catch (e) { + isException(e, Cr.NS_ERROR_NOT_AVAILABLE); + } + + do_test_pending(); + try { + srv.stop({ + onStopped() { + try { + do_test_pending(); + run_test_3(); + } finally { + do_test_finished(); + } + }, + }); + } catch (e) { + do_throw("error stopping with an object: " + e); + } +} + +function run_test_3() { + dumpn("*** run_test_3"); + + do_test_finished(); + + srv.start_ipv6(PORT); + + do_test_pending(); + try { + srv.stop().then(function () { + try { + do_test_pending(); + run_test_4(); + } finally { + do_test_finished(); + } + }); + } catch (e) { + do_throw("error stopping with an object: " + e); + } +} + +function run_test_4() { + dumpn("*** run_test_4"); + + do_test_finished(); + + srv.registerPathHandler("/handle", handle); + srv.start_ipv6(PORT); + + // Don't rely on the exact (but implementation-constant) sequence of events + // as it currently exists by making either run_test_5 or serverStopped handle + // the final shutdown. + do_test_pending(); + + runHttpTests([new Test(PREPATH + "/handle")], run_test_5); +} + +var testsComplete = false; + +function run_test_5() { + dumpn("*** run_test_5"); + + testsComplete = true; + if (stopped) { + do_test_finished(); + } +} + +const INTERVAL = 500; + +function handle(request, response) { + response.processAsync(); + + dumpn("*** stopping server..."); + srv.stop(serverStopped); + + callLater(INTERVAL, function () { + Assert.ok(!stopped); + + callLater(INTERVAL, function () { + Assert.ok(!stopped); + response.finish(); + + try { + response.processAsync(); + do_throw("late processAsync didn't throw?"); + } catch (e) { + isException(e, Cr.NS_ERROR_UNEXPECTED); + } + }); + }); +} + +var stopped = false; +function serverStopped() { + dumpn("*** server really, fully shut down now"); + stopped = true; + if (testsComplete) { + do_test_finished(); + } +} diff --git a/netwerk/test/httpserver/test/xpcshell.ini b/netwerk/test/httpserver/test/xpcshell.ini new file mode 100644 index 0000000000..08173134e4 --- /dev/null +++ b/netwerk/test/httpserver/test/xpcshell.ini @@ -0,0 +1,38 @@ +[DEFAULT] +head = head_utils.js +support-files = data/** ../httpd.js + +[test_async_response_sending.js] +[test_basic_functionality.js] +[test_body_length.js] +[test_byte_range.js] +[test_cern_meta.js] +[test_default_index_handler.js] +[test_empty_body.js] +[test_errorhandler_exception.js] +[test_header_array.js] +[test_headers.js] +[test_host.js] +skip-if = os == 'mac' +run-sequentially = Reusing same server on different specific ports. +[test_host_identity.js] +[test_linedata.js] +[test_load_module.js] +[test_name_scheme.js] +[test_processasync.js] +[test_qi.js] +[test_registerdirectory.js] +[test_registerfile.js] +[test_registerprefix.js] +[test_request_line_split_in_two_packets.js] +[test_response_write.js] +[test_seizepower.js] +skip-if = (os == 'mac' && socketprocess_networking) +[test_setindexhandler.js] +[test_setstatusline.js] +[test_sjs.js] +[test_sjs_object_state.js] +[test_sjs_state.js] +[test_sjs_throwing_exceptions.js] +[test_start_stop.js] +[test_start_stop_ipv6.js] diff --git a/netwerk/test/marionette/manifest.ini b/netwerk/test/marionette/manifest.ini new file mode 100644 index 0000000000..e2025ab3c9 --- /dev/null +++ b/netwerk/test/marionette/manifest.ini @@ -0,0 +1 @@ +[test_purge_http_cache_at_shutdown.py] diff --git a/netwerk/test/marionette/test_purge_http_cache_at_shutdown.py b/netwerk/test/marionette/test_purge_http_cache_at_shutdown.py new file mode 100644 index 0000000000..ee27e29e8d --- /dev/null +++ b/netwerk/test/marionette/test_purge_http_cache_at_shutdown.py @@ -0,0 +1,125 @@ +# 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 os +from pathlib import Path + +from marionette_driver import Wait +from marionette_harness import MarionetteTestCase + + +class PurgeHTTPCacheAtShutdownTestCase(MarionetteTestCase): + def setUp(self): + super().setUp() + self.marionette.enforce_gecko_prefs( + { + "privacy.sanitize.sanitizeOnShutdown": True, + "privacy.clearOnShutdown.cache": True, + "network.cache.shutdown_purge_in_background_task": True, + } + ) + + self.profile_path = Path(self.marionette.profile_path) + self.cache_path = self.profile_path.joinpath("cache2") + + def tearDown(self): + self.marionette.cleanup() + super().tearDown() + + def cacheDirExists(self): + return self.cache_path.exists() + + def renamedDirExists(self): + return any( + child.name.endswith(".purge.bg_rm") for child in self.profile_path.iterdir() + ) + + def initLockDir(self): + self.lock_dir = None + with self.marionette.using_context("chrome"): + path = self.marionette.execute_script( + """ + return Services.dirsvc.get("UpdRootD", Ci.nsIFile).parent.parent.path; + """ + ) + self.lock_dir = Path(path) + + def assertNoLocks(self): + locks = [ + x + for x in self.lock_dir.iterdir() + if x.is_file() and "-cachePurge" in x.name + ] + self.assertEqual(locks, [], "All locks should have been removed") + + # Tests are run in lexicographical order, so test_a* is run first to + # cleanup locks that may be there from previous runs. + def test_asetup(self): + self.initLockDir() + + self.marionette.quit() + + Wait(self.marionette, timeout=60).until( + lambda _: not self.cacheDirExists() and not self.renamedDirExists(), + message="Cache directory must be removed after orderly shutdown", + ) + + # delete locks from previous runs + locks = [ + x + for x in self.lock_dir.iterdir() + if x.is_file() and "-cachePurge" in x.name + ] + for lock in locks: + os.remove(lock) + # all locks should have been removed successfully. + self.assertNoLocks() + + def test_ensure_cache_purge_after_in_app_quit(self): + self.assertTrue(self.cacheDirExists(), "Cache directory must exist") + self.initLockDir() + + self.marionette.quit() + + Wait(self.marionette, timeout=60).until( + lambda _: not self.cacheDirExists() and not self.renamedDirExists(), + message="Cache directory must be removed after orderly shutdown", + ) + + self.assertNoLocks() + + def test_longstanding_cache_purge_after_in_app_quit(self): + self.assertTrue(self.cacheDirExists(), "Cache directory must exist") + self.initLockDir() + + self.marionette.set_pref( + "toolkit.background_tasks.remove_directory.testing.sleep_ms", 5000 + ) + + self.marionette.quit() + + Wait(self.marionette, timeout=60).until( + lambda _: not self.cacheDirExists() and not self.renamedDirExists(), + message="Cache directory must be removed after orderly shutdown", + ) + + self.assertNoLocks() + + def test_ensure_cache_purge_after_forced_restart(self): + """ + Doing forced restart here to prevent the shutdown phase purging and only allow startup + phase one, via `CacheFileIOManager::OnDelayedStartupFinished`. + """ + self.profile_path.joinpath("foo.purge.bg_rm").mkdir() + self.initLockDir() + + self.marionette.restart(in_app=False) + + Wait(self.marionette, timeout=60).until( + lambda _: not self.renamedDirExists(), + message="Directories with .purge.bg_rm postfix must be removed at startup after" + "disorderly shutdown", + ) + + self.assertNoLocks() diff --git a/netwerk/test/mochitests/beltzner.jpg b/netwerk/test/mochitests/beltzner.jpg Binary files differnew file mode 100644 index 0000000000..75849bc40d --- /dev/null +++ b/netwerk/test/mochitests/beltzner.jpg diff --git a/netwerk/test/mochitests/beltzner.jpg^headers^ b/netwerk/test/mochitests/beltzner.jpg^headers^ new file mode 100644 index 0000000000..cb51f27018 --- /dev/null +++ b/netwerk/test/mochitests/beltzner.jpg^headers^ @@ -0,0 +1,3 @@ +Cache-Control: no-store +Set-Cookie: mike=beltzer + diff --git a/netwerk/test/mochitests/empty.html b/netwerk/test/mochitests/empty.html new file mode 100644 index 0000000000..e60f5abdf4 --- /dev/null +++ b/netwerk/test/mochitests/empty.html @@ -0,0 +1,16 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> + +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + This page does nothing. If the loading page managed to load this, the test + probably succeeded. +</body> +</html> diff --git a/netwerk/test/mochitests/file_1331680.js b/netwerk/test/mochitests/file_1331680.js new file mode 100644 index 0000000000..637c1f2a6d --- /dev/null +++ b/netwerk/test/mochitests/file_1331680.js @@ -0,0 +1,23 @@ +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +var observer = { + observe(subject, topic, data) { + if (topic == "cookie-changed") { + let cookie = subject.QueryInterface(Ci.nsICookie); + sendAsyncMessage("cookieName", cookie.name + "=" + cookie.value); + sendAsyncMessage("cookieOperation", data); + } + }, +}; + +addMessageListener("createObserver", function (e) { + Services.obs.addObserver(observer, "cookie-changed"); + sendAsyncMessage("createObserver:return"); +}); + +addMessageListener("removeObserver", function (e) { + Services.obs.removeObserver(observer, "cookie-changed"); + sendAsyncMessage("removeObserver:return"); +}); diff --git a/netwerk/test/mochitests/file_1502055.sjs b/netwerk/test/mochitests/file_1502055.sjs new file mode 100644 index 0000000000..71a4ddfa66 --- /dev/null +++ b/netwerk/test/mochitests/file_1502055.sjs @@ -0,0 +1,17 @@ +function handleRequest(request, response) { + var count = parseInt(getState("count")); + if (!count) { + count = 0; + } + + if (count == 0) { + response.setStatusLine(request.httpVersion, "200", "OK"); + response.setHeader("Content-Type", "text/html", false); + response.write("<html><body>Hello world!</body></html>"); + setState("count", "1"); + return; + } + + response.setStatusLine(request.httpVersion, "304", "Not Modified"); + response.setHeader("Clear-Site-Data", '"storage"'); +} diff --git a/netwerk/test/mochitests/file_1503201.sjs b/netwerk/test/mochitests/file_1503201.sjs new file mode 100644 index 0000000000..ac5c304103 --- /dev/null +++ b/netwerk/test/mochitests/file_1503201.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Bearer realm="foo"'); +} diff --git a/netwerk/test/mochitests/file_chromecommon.js b/netwerk/test/mochitests/file_chromecommon.js new file mode 100644 index 0000000000..986a9d7ef1 --- /dev/null +++ b/netwerk/test/mochitests/file_chromecommon.js @@ -0,0 +1,15 @@ +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +// eslint-disable-next-line mozilla/use-services +let cs = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + +addMessageListener("getCookieCountAndClear", () => { + let count = cs.cookies.length; + cs.removeAll(); + + sendAsyncMessage("getCookieCountAndClear:return", { count }); +}); + +cs.removeAll(); diff --git a/netwerk/test/mochitests/file_documentcookie_maxage_chromescript.js b/netwerk/test/mochitests/file_documentcookie_maxage_chromescript.js new file mode 100644 index 0000000000..dc1e8eb05f --- /dev/null +++ b/netwerk/test/mochitests/file_documentcookie_maxage_chromescript.js @@ -0,0 +1,45 @@ +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +function getCookieService() { + // eslint-disable-next-line mozilla/use-services + return Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); +} + +function getCookies(cs) { + let cookies = []; + for (let cookie of cs.cookies) { + cookies.push({ + host: cookie.host, + path: cookie.path, + name: cookie.name, + value: cookie.value, + expires: cookie.expires, + }); + } + return cookies; +} + +function removeAllCookies(cs) { + cs.removeAll(); +} + +addMessageListener("init", _ => { + let cs = getCookieService(); + removeAllCookies(cs); + sendAsyncMessage("init:return"); +}); + +addMessageListener("getCookies", _ => { + let cs = getCookieService(); + let cookies = getCookies(cs); + removeAllCookies(cs); + sendAsyncMessage("getCookies:return", { cookies }); +}); + +addMessageListener("shutdown", _ => { + let cs = getCookieService(); + removeAllCookies(cs); + sendAsyncMessage("shutdown:return"); +}); diff --git a/netwerk/test/mochitests/file_domain_hierarchy_inner.html b/netwerk/test/mochitests/file_domain_hierarchy_inner.html new file mode 100644 index 0000000000..713432196f --- /dev/null +++ b/netwerk/test/mochitests/file_domain_hierarchy_inner.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="text/javascript"> + document.cookie = "can=has"; + + // send a message to our test document, to say we're done loading + window.opener.postMessage("message", "http://mochi.test:8888"); + </script> +<body> +<iframe name="frame1" src="https://example.com/tests/netwerk/test/mochitests/file_domain_hierarchy_inner_inner.html"></iframe> +</body> +</html> diff --git a/netwerk/test/mochitests/file_domain_hierarchy_inner.html^headers^ b/netwerk/test/mochitests/file_domain_hierarchy_inner.html^headers^ new file mode 100644 index 0000000000..a56be562a4 --- /dev/null +++ b/netwerk/test/mochitests/file_domain_hierarchy_inner.html^headers^ @@ -0,0 +1,4 @@ +Set-Cookie: meta=tag +Cache-Control: no-cache, no-store, must-revalidate +Pragma: no-cache +Expires: 0 diff --git a/netwerk/test/mochitests/file_domain_hierarchy_inner_inner.html b/netwerk/test/mochitests/file_domain_hierarchy_inner_inner.html new file mode 100644 index 0000000000..dfceac2c6c --- /dev/null +++ b/netwerk/test/mochitests/file_domain_hierarchy_inner_inner.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="text/javascript"> + document.cookie = "can2=has2"; + + // send a message to our test document, to say we're done loading + window.parent.opener.postMessage("message", "http://mochi.test:8888"); + </script> +<body> +<iframe name="frame1" src="https://example.org/tests/netwerk/test/mochitests/file_domain_hierarchy_inner_inner_inner.html"></iframe> +</body> +</html> diff --git a/netwerk/test/mochitests/file_domain_hierarchy_inner_inner.html^headers^ b/netwerk/test/mochitests/file_domain_hierarchy_inner_inner.html^headers^ new file mode 100644 index 0000000000..1ab9133473 --- /dev/null +++ b/netwerk/test/mochitests/file_domain_hierarchy_inner_inner.html^headers^ @@ -0,0 +1,4 @@ +Set-Cookie: meta2=tag2 +Cache-Control: no-cache, no-store, must-revalidate +Pragma: no-cache +Expires: 0 diff --git a/netwerk/test/mochitests/file_domain_hierarchy_inner_inner_inner.html b/netwerk/test/mochitests/file_domain_hierarchy_inner_inner_inner.html new file mode 100644 index 0000000000..d306efb1c0 --- /dev/null +++ b/netwerk/test/mochitests/file_domain_hierarchy_inner_inner_inner.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="text/javascript"> + document.cookie = "can3=has3"; + + // send a message to our test document, to say we're done loading + window.parent.parent.opener.postMessage("message", "http://mochi.test:8888"); + </script> +</head> +<body> +</body> +</html> diff --git a/netwerk/test/mochitests/file_domain_hierarchy_inner_inner_inner.html^headers^ b/netwerk/test/mochitests/file_domain_hierarchy_inner_inner_inner.html^headers^ new file mode 100644 index 0000000000..add3336ec9 --- /dev/null +++ b/netwerk/test/mochitests/file_domain_hierarchy_inner_inner_inner.html^headers^ @@ -0,0 +1 @@ +Set-Cookie: meta3=tag3 diff --git a/netwerk/test/mochitests/file_domain_inner.html b/netwerk/test/mochitests/file_domain_inner.html new file mode 100644 index 0000000000..30a0821980 --- /dev/null +++ b/netwerk/test/mochitests/file_domain_inner.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="text/javascript"> + document.cookie = "can=has"; + + // send a message to our test document, to say we're done loading + window.opener.postMessage("message", "http://mochi.test:8888"); + </script> +<body> +<iframe name="frame1" src="https://example.org/tests/netwerk/test/mochitests/file_domain_inner_inner.html"></iframe> +</body> +</html> diff --git a/netwerk/test/mochitests/file_domain_inner.html^headers^ b/netwerk/test/mochitests/file_domain_inner.html^headers^ new file mode 100644 index 0000000000..a56be562a4 --- /dev/null +++ b/netwerk/test/mochitests/file_domain_inner.html^headers^ @@ -0,0 +1,4 @@ +Set-Cookie: meta=tag +Cache-Control: no-cache, no-store, must-revalidate +Pragma: no-cache +Expires: 0 diff --git a/netwerk/test/mochitests/file_domain_inner_inner.html b/netwerk/test/mochitests/file_domain_inner_inner.html new file mode 100644 index 0000000000..5850e3fa0f --- /dev/null +++ b/netwerk/test/mochitests/file_domain_inner_inner.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="text/javascript"> + document.cookie = "can2=has2"; + + // send a message to our test document, to say we're done loading + window.parent.opener.postMessage("message", "http://mochi.test:8888"); + </script> +</head> +<body> +</body> +</html> diff --git a/netwerk/test/mochitests/file_domain_inner_inner.html^headers^ b/netwerk/test/mochitests/file_domain_inner_inner.html^headers^ new file mode 100644 index 0000000000..1ab9133473 --- /dev/null +++ b/netwerk/test/mochitests/file_domain_inner_inner.html^headers^ @@ -0,0 +1,4 @@ +Set-Cookie: meta2=tag2 +Cache-Control: no-cache, no-store, must-revalidate +Pragma: no-cache +Expires: 0 diff --git a/netwerk/test/mochitests/file_iframe_allow_same_origin.html b/netwerk/test/mochitests/file_iframe_allow_same_origin.html new file mode 100644 index 0000000000..c34187bb0a --- /dev/null +++ b/netwerk/test/mochitests/file_iframe_allow_same_origin.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<script> +document.cookie = "if2_1=if2_val1"; +document.cookie = "if2_2=if2_val2"; +window.parent.postMessage(document.cookie, "*"); +</script> +</html> diff --git a/netwerk/test/mochitests/file_iframe_allow_scripts.html b/netwerk/test/mochitests/file_iframe_allow_scripts.html new file mode 100644 index 0000000000..01504841a1 --- /dev/null +++ b/netwerk/test/mochitests/file_iframe_allow_scripts.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<script> +document.cookie = "if1=if1_val"; +</script> +</html> diff --git a/netwerk/test/mochitests/file_image_inner.html b/netwerk/test/mochitests/file_image_inner.html new file mode 100644 index 0000000000..6daf7224da --- /dev/null +++ b/netwerk/test/mochitests/file_image_inner.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="text/javascript"> + document.cookie = "can=has"; + + // send a message to our test document, to say we're done loading + window.opener.postMessage("message", "http://mochi.test:8888"); + </script> +</head> +<body> +<iframe name="frame1" src="https://example.org/tests/netwerk/test/mochitests/file_image_inner_inner.html"></iframe> +</body> +</html> diff --git a/netwerk/test/mochitests/file_image_inner.html^headers^ b/netwerk/test/mochitests/file_image_inner.html^headers^ new file mode 100644 index 0000000000..a56be562a4 --- /dev/null +++ b/netwerk/test/mochitests/file_image_inner.html^headers^ @@ -0,0 +1,4 @@ +Set-Cookie: meta=tag +Cache-Control: no-cache, no-store, must-revalidate +Pragma: no-cache +Expires: 0 diff --git a/netwerk/test/mochitests/file_image_inner_inner.html b/netwerk/test/mochitests/file_image_inner_inner.html new file mode 100644 index 0000000000..1e605f1a25 --- /dev/null +++ b/netwerk/test/mochitests/file_image_inner_inner.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <link rel="stylesheet" type="text/css" media="all" href="https://example.org/tests/netwerk/test/mochitests/test1.css" /> + <link rel="stylesheet" type="text/css" media="all" href="https://example.com/tests/netwerk/test/mochitests/test2.css" /> + <script type="text/javascript"> + function runTest() { + document.cookie = "can2=has2"; + + // send a message to our test document, to say we're done loading + window.parent.opener.postMessage("message", "http://mochi.test:8888"); + } + </script> +</head> +<body> +<img src="https://example.org/tests/netwerk/test/mochitests/image1.png" onload="runTest()" /> +<img src="https://example.com/tests/netwerk/test/mochitests/image2.png" onload="runTest()" /> +</body> +</html> diff --git a/netwerk/test/mochitests/file_image_inner_inner.html^headers^ b/netwerk/test/mochitests/file_image_inner_inner.html^headers^ new file mode 100644 index 0000000000..1ab9133473 --- /dev/null +++ b/netwerk/test/mochitests/file_image_inner_inner.html^headers^ @@ -0,0 +1,4 @@ +Set-Cookie: meta2=tag2 +Cache-Control: no-cache, no-store, must-revalidate +Pragma: no-cache +Expires: 0 diff --git a/netwerk/test/mochitests/file_lnk.lnk b/netwerk/test/mochitests/file_lnk.lnk Binary files differnew file mode 100644 index 0000000000..abce7587d2 --- /dev/null +++ b/netwerk/test/mochitests/file_lnk.lnk diff --git a/netwerk/test/mochitests/file_loadflags_inner.html b/netwerk/test/mochitests/file_loadflags_inner.html new file mode 100644 index 0000000000..5fc7e8d913 --- /dev/null +++ b/netwerk/test/mochitests/file_loadflags_inner.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="text/javascript"> + function runTest() { + document.cookie = "can=has"; + + // send a message to our test document, to say we're done loading + window.opener.postMessage("f_lf_i msg data img", "http://mochi.test:8888"); + } + </script> +</head> +<body onload="window.opener.postMessage('f_lf_i msg data page', 'http://mochi.test:8888');"> +<img src="http://example.org/tests/netwerk/test/mochitests/beltzner.jpg" onload="runTest()" /> +</body> +</html> diff --git a/netwerk/test/mochitests/file_loadflags_inner.html^headers^ b/netwerk/test/mochitests/file_loadflags_inner.html^headers^ new file mode 100644 index 0000000000..a56be562a4 --- /dev/null +++ b/netwerk/test/mochitests/file_loadflags_inner.html^headers^ @@ -0,0 +1,4 @@ +Set-Cookie: meta=tag +Cache-Control: no-cache, no-store, must-revalidate +Pragma: no-cache +Expires: 0 diff --git a/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs b/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs new file mode 100644 index 0000000000..0c89f7d538 --- /dev/null +++ b/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs @@ -0,0 +1,109 @@ +/* + * Redirect handler specifically for the needs of: + * Bug 1194052 - Append Principal to RedirectChain within LoadInfo before the channel is succesfully openend + */ + +function createIframeContent(aQuery) { + var content = ` + <!DOCTYPE HTML> + <html> + <head><meta charset="utf-8"> + <title>Bug 1194052 - LoadInfo redirect chain subtest</title> + </head> + <body> + <script type="text/javascript"> + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?${aQuery}"); + myXHR.onload = function() { + var loadinfo = SpecialPowers.wrap(myXHR).channel.loadInfo; + var redirectChain = loadinfo.redirectChain; + var redirectChainIncludingInternalRedirects = loadinfo.redirectChainIncludingInternalRedirects; + var resultOBJ = { redirectChain : [], redirectChainIncludingInternalRedirects : [] }; + for (var i = 0; i < redirectChain.length; i++) { + resultOBJ.redirectChain.push(redirectChain[i].principal.spec); + } + for (var i = 0; i < redirectChainIncludingInternalRedirects.length; i++) { + resultOBJ.redirectChainIncludingInternalRedirects.push(redirectChainIncludingInternalRedirects[i].principal.spec); + } + var loadinfoJSON = JSON.stringify(resultOBJ); + window.parent.postMessage({ loadinfo: loadinfoJSON }, "*"); + } + myXHR.onerror = function() { + var resultOBJ = { redirectChain : [], redirectChainIncludingInternalRedirects : [] }; + var loadinfoJSON = JSON.stringify(resultOBJ); + window.parent.postMessage({ loadinfo: loadinfoJSON }, "*"); + } + myXHR.send(); + </script> + </body> + </html>`; + + return content; +} + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + var queryString = request.queryString; + + if ( + queryString == "iframe-redir-https-2" || + queryString == "iframe-redir-err-2" + ) { + var query = queryString.replace("iframe-", ""); + // send upgrade-insecure-requests CSP header + response.setHeader("Content-Type", "text/html", false); + response.setHeader( + "Content-Security-Policy", + "upgrade-insecure-requests", + false + ); + response.write(createIframeContent(query)); + return; + } + + // at the end of the redirectchain we return some text + // for sanity checking + if (queryString == "redir-0" || queryString == "redir-https-0") { + response.setHeader("Content-Type", "text/html", false); + response.write("checking redirectchain"); + return; + } + + // special case redir-err-1 and return an error to trigger the fallback + if (queryString == "redir-err-1") { + response.setStatusLine("1.1", 404, "Bad request"); + return; + } + + // must be a redirect + var newLocation = ""; + switch (queryString) { + case "redir-err-2": + newLocation = + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-err-1"; + break; + + case "redir-https-2": + newLocation = + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-1"; + break; + + case "redir-https-1": + newLocation = + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-0"; + break; + + case "redir-2": + newLocation = + "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-1"; + break; + + case "redir-1": + newLocation = + "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-0"; + break; + } + + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", newLocation, false); +} diff --git a/netwerk/test/mochitests/file_localhost_inner.html b/netwerk/test/mochitests/file_localhost_inner.html new file mode 100644 index 0000000000..d291370a47 --- /dev/null +++ b/netwerk/test/mochitests/file_localhost_inner.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="text/javascript"> + document.cookie = "can=has"; + + // send a message to our test document, to say we're done loading + window.opener.postMessage("message", "http://mochi.test:8888"); + </script> +<body> +<iframe name="frame1" src="http://mochi.test:8888/tests/netwerk/test/mochitests/file_domain_inner_inner.html"></iframe> +</body> +</html> diff --git a/netwerk/test/mochitests/file_localhost_inner.html^headers^ b/netwerk/test/mochitests/file_localhost_inner.html^headers^ new file mode 100644 index 0000000000..a56be562a4 --- /dev/null +++ b/netwerk/test/mochitests/file_localhost_inner.html^headers^ @@ -0,0 +1,4 @@ +Set-Cookie: meta=tag +Cache-Control: no-cache, no-store, must-revalidate +Pragma: no-cache +Expires: 0 diff --git a/netwerk/test/mochitests/file_loopback_inner.html b/netwerk/test/mochitests/file_loopback_inner.html new file mode 100644 index 0000000000..91aa1d89c5 --- /dev/null +++ b/netwerk/test/mochitests/file_loopback_inner.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="text/javascript"> + document.cookie = "can=has"; + + // send a message to our test document, to say we're done loading + window.opener.postMessage("message", "http://mochi.test:8888"); + </script> +<body> +<iframe name="frame1" src="http://127.0.0.1:8888/tests/netwerk/test/mochitests/file_domain_inner_inner.html"></iframe> +</body> +</html> diff --git a/netwerk/test/mochitests/file_loopback_inner.html^headers^ b/netwerk/test/mochitests/file_loopback_inner.html^headers^ new file mode 100644 index 0000000000..a56be562a4 --- /dev/null +++ b/netwerk/test/mochitests/file_loopback_inner.html^headers^ @@ -0,0 +1,4 @@ +Set-Cookie: meta=tag +Cache-Control: no-cache, no-store, must-revalidate +Pragma: no-cache +Expires: 0 diff --git a/netwerk/test/mochitests/file_subdomain_inner.html b/netwerk/test/mochitests/file_subdomain_inner.html new file mode 100644 index 0000000000..a48f36a7de --- /dev/null +++ b/netwerk/test/mochitests/file_subdomain_inner.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="text/javascript"> + document.cookie = "can=has"; + + // send a message to our test document, to say we're done loading + window.opener.postMessage("message", "http://mochi.test:8888"); + </script> +<body> +<iframe name="frame1" src="https://test2.example.org/tests/netwerk/test/mochitests/file_domain_inner_inner.html"></iframe> +</body> +</html> diff --git a/netwerk/test/mochitests/file_subdomain_inner.html^headers^ b/netwerk/test/mochitests/file_subdomain_inner.html^headers^ new file mode 100644 index 0000000000..a56be562a4 --- /dev/null +++ b/netwerk/test/mochitests/file_subdomain_inner.html^headers^ @@ -0,0 +1,4 @@ +Set-Cookie: meta=tag +Cache-Control: no-cache, no-store, must-revalidate +Pragma: no-cache +Expires: 0 diff --git a/netwerk/test/mochitests/file_testcommon.js b/netwerk/test/mochitests/file_testcommon.js new file mode 100644 index 0000000000..8db5c644d4 --- /dev/null +++ b/netwerk/test/mochitests/file_testcommon.js @@ -0,0 +1,85 @@ +"use strict"; + +const SCRIPT_URL = SimpleTest.getTestFileURL("file_chromecommon.js"); + +var gExpectedCookies; +var gExpectedLoads; + +var gPopup; + +var gScript; + +var gLoads = 0; + +function setupTest(uri, cookies, loads) { + SimpleTest.waitForExplicitFinish(); + + var prefSet = new Promise(resolve => { + SpecialPowers.pushPrefEnv( + { + set: [ + ["network.cookie.cookieBehavior", 1], + // cookieBehavior 1 allows cookies from chrome script if we enable + // exceptions. + ["network.cookie.rejectForeignWithExceptions.enabled", false], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ], + }, + resolve + ); + }); + + gScript = SpecialPowers.loadChromeScript(SCRIPT_URL); + gExpectedCookies = cookies; + gExpectedLoads = loads; + + // Listen for MessageEvents. + window.addEventListener("message", messageReceiver); + + prefSet.then(() => { + // load a window which contains an iframe; each will attempt to set + // cookies from their respective domains. + gPopup = window.open(uri, "hai", "width=100,height=100"); + }); +} + +function finishTest() { + gScript.destroy(); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); +} + +/** Receives MessageEvents to this window. */ +// Count and check loads. +function messageReceiver(evt) { + is(evt.data, "message", "message data received from popup"); + if (evt.data != "message") { + gPopup.close(); + window.removeEventListener("message", messageReceiver); + + finishTest(); + return; + } + + // only run the test when all our children are done loading & setting cookies + if (++gLoads == gExpectedLoads) { + gPopup.close(); + window.removeEventListener("message", messageReceiver); + + runTest(); + } +} + +// runTest() is run by messageReceiver(). +// Count and check cookies. +function runTest() { + // set a cookie from a domain of "localhost" + document.cookie = "oh=hai"; + + gScript.addMessageListener("getCookieCountAndClear:return", ({ count }) => { + is(count, gExpectedCookies, "total number of cookies"); + finishTest(); + }); + gScript.sendAsyncMessage("getCookieCountAndClear"); +} diff --git a/netwerk/test/mochitests/file_testloadflags.js b/netwerk/test/mochitests/file_testloadflags.js new file mode 100644 index 0000000000..56c9596a41 --- /dev/null +++ b/netwerk/test/mochitests/file_testloadflags.js @@ -0,0 +1,130 @@ +"use strict"; + +const SCRIPT_URL = SimpleTest.getTestFileURL( + "file_testloadflags_chromescript.js" +); + +let gScript; +var gExpectedCookies; +var gExpectedHeaders; +var gExpectedLoads; + +var gObs; +var gPopup; + +var gHeaders = 0; +var gLoads = 0; + +// setupTest() is run from 'onload='. +function setupTest(uri, domain, cookies, loads, headers) { + info( + "setupTest uri: " + + uri + + " domain: " + + domain + + " cookies: " + + cookies + + " loads: " + + loads + + " headers: " + + headers + ); + + SimpleTest.waitForExplicitFinish(); + + var prefSet = new Promise(resolve => { + SpecialPowers.pushPrefEnv( + { + set: [ + ["network.cookie.cookieBehavior", 1], + ["network.cookie.sameSite.schemeful", false], + ], + }, + resolve + ); + }); + + gExpectedCookies = cookies; + gExpectedLoads = loads; + gExpectedHeaders = headers; + + gScript = SpecialPowers.loadChromeScript(SCRIPT_URL); + gScript.addMessageListener("info", ({ str }) => info(str)); + gScript.addMessageListener("ok", ({ c, m }) => ok(c, m)); + gScript.addMessageListener("observer:gotCookie", ({ cookie, uri }) => { + isnot( + cookie.indexOf("oh=hai"), + -1, + "cookie 'oh=hai' is in header for " + uri + ); + ++gHeaders; + }); + + var scriptReady = new Promise(resolve => { + gScript.addMessageListener("init:return", resolve); + gScript.sendAsyncMessage("init", { domain }); + }); + + // Listen for MessageEvents. + window.addEventListener("message", messageReceiver); + + Promise.all([prefSet, scriptReady]).then(() => { + // load a window which contains an iframe; each will attempt to set + // cookies from their respective domains. + gPopup = window.open(uri, "hai", "width=100,height=100"); + }); +} + +function finishTest() { + gScript.addMessageListener("shutdown:return", () => { + gScript.destroy(); + SimpleTest.finish(); + }); + gScript.sendAsyncMessage("shutdown"); +} + +/** Receives MessageEvents to this window. */ +// Count and check loads. +function messageReceiver(evt) { + ok( + evt.data == "f_lf_i msg data img" || evt.data == "f_lf_i msg data page", + "message data received from popup" + ); + if (evt.data == "f_lf_i msg data img") { + info("message data received from popup for image"); + } + if (evt.data == "f_lf_i msg data page") { + info("message data received from popup for page"); + } + if (evt.data != "f_lf_i msg data img" && evt.data != "f_lf_i msg data page") { + info("got this message but don't know what it is " + evt.data); + gPopup.close(); + window.removeEventListener("message", messageReceiver); + + finishTest(); + return; + } + + // only run the test when all our children are done loading & setting cookies + if (++gLoads == gExpectedLoads) { + gPopup.close(); + window.removeEventListener("message", messageReceiver); + + runTest(); + } +} + +// runTest() is run by messageReceiver(). +// Check headers, and count and check cookies. +function runTest() { + // set a cookie from a domain of "localhost" + document.cookie = "o=noes"; + + is(gHeaders, gExpectedHeaders, "number of observed request headers"); + gScript.addMessageListener("getCookieCount:return", ({ count }) => { + is(count, gExpectedCookies, "total number of cookies"); + finishTest(); + }); + + gScript.sendAsyncMessage("getCookieCount"); +} diff --git a/netwerk/test/mochitests/file_testloadflags_chromescript.js b/netwerk/test/mochitests/file_testloadflags_chromescript.js new file mode 100644 index 0000000000..a74920d2c2 --- /dev/null +++ b/netwerk/test/mochitests/file_testloadflags_chromescript.js @@ -0,0 +1,144 @@ +/* eslint-env mozilla/chrome-script */ +/* eslint-disable mozilla/use-services */ + +"use strict"; + +var gObs; + +function info(s) { + sendAsyncMessage("info", { str: String(s) }); +} + +function ok(c, m) { + sendAsyncMessage("ok", { c, m }); +} + +function is(a, b, m) { + ok(Object.is(a, b), m + " (" + a + " === " + b + ")"); +} + +// Count headers. +function obs() { + info("adding observer"); + + this.os = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + this.os.addObserver(this, "http-on-modify-request"); +} + +obs.prototype = { + observe(theSubject, theTopic, theData) { + info("theSubject " + theSubject); + info("theTopic " + theTopic); + info("theData " + theData); + + var channel = theSubject.QueryInterface(Ci.nsIHttpChannel); + info("channel " + channel); + try { + info("channel.URI " + channel.URI); + info("channel.URI.spec " + channel.URI.spec); + channel.visitRequestHeaders({ + visitHeader(aHeader, aValue) { + info(aHeader + ": " + aValue); + }, + }); + } catch (err) { + ok(false, "catch error " + err); + } + + // Ignore notifications we don't care about (like favicons) + if ( + !channel.URI.spec.includes( + "http://example.org/tests/netwerk/test/mochitests/" + ) + ) { + info("ignoring this one"); + return; + } + + sendAsyncMessage("observer:gotCookie", { + cookie: channel.getRequestHeader("Cookie"), + uri: channel.URI.spec, + }); + }, + + remove() { + info("removing observer"); + + this.os.removeObserver(this, "http-on-modify-request"); + this.os = null; + }, +}; + +function getCookieCount(cs) { + let count = 0; + for (let cookie of cs.cookies) { + info("cookie: " + cookie); + info( + "cookie host " + + cookie.host + + " path " + + cookie.path + + " name " + + cookie.name + + " value " + + cookie.value + + " isSecure " + + cookie.isSecure + + " expires " + + cookie.expires + ); + ++count; + } + + return count; +} + +addMessageListener("init", ({ domain }) => { + let cs = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + + info("we are going to remove these cookies"); + + let count = getCookieCount(cs); + info(count + " cookies"); + + cs.removeAll(); + cs.add( + domain, + "/", + "oh", + "hai", + false, + false, + true, + Math.pow(2, 62), + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + is( + cs.countCookiesFromHost(domain), + 1, + "number of cookies for domain " + domain + ); + + gObs = new obs(); + sendAsyncMessage("init:return"); +}); + +addMessageListener("getCookieCount", () => { + let cs = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + let count = getCookieCount(cs); + + cs.removeAll(); + sendAsyncMessage("getCookieCount:return", { count }); +}); + +addMessageListener("shutdown", () => { + gObs.remove(); + + let cs = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + cs.removeAll(); + sendAsyncMessage("shutdown:return"); +}); diff --git a/netwerk/test/mochitests/iframe_1502055.html b/netwerk/test/mochitests/iframe_1502055.html new file mode 100644 index 0000000000..7f3e915978 --- /dev/null +++ b/netwerk/test/mochitests/iframe_1502055.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<body> +<script type="application/javascript"> + +function info(msg) { + parent.postMessage({type: "info", msg }, "*"); +} + +let registration; + +info("Registering a ServiceWorker"); +navigator.serviceWorker.register('sw_1502055.js', {scope: "foo/"}) +.then(reg => { + registration = reg; + info("Fetching a resource"); + return fetch("file_1502055.sjs") +}).then(r => r.text()) +.then(() => { + info("Fetching a resource, again"); + return fetch("file_1502055.sjs") +}).then(r => r.text()).then(() => { + info("Unregistering the ServiceWorker"); + return registration.unregister(); +}) +.then(() => { + parent.postMessage({ type: "finish" }, "*"); +}); + +</script> + +</body> +</html> diff --git a/netwerk/test/mochitests/image1.png b/netwerk/test/mochitests/image1.png Binary files differnew file mode 100644 index 0000000000..272d67c0ce --- /dev/null +++ b/netwerk/test/mochitests/image1.png diff --git a/netwerk/test/mochitests/image1.png^headers^ b/netwerk/test/mochitests/image1.png^headers^ new file mode 100644 index 0000000000..2390289e01 --- /dev/null +++ b/netwerk/test/mochitests/image1.png^headers^ @@ -0,0 +1,3 @@ +Cache-Control: no-store +Set-Cookie: foo=bar + diff --git a/netwerk/test/mochitests/image2.png b/netwerk/test/mochitests/image2.png Binary files differnew file mode 100644 index 0000000000..272d67c0ce --- /dev/null +++ b/netwerk/test/mochitests/image2.png diff --git a/netwerk/test/mochitests/image2.png^headers^ b/netwerk/test/mochitests/image2.png^headers^ new file mode 100644 index 0000000000..6c0eea5ab6 --- /dev/null +++ b/netwerk/test/mochitests/image2.png^headers^ @@ -0,0 +1,3 @@ +Cache-Control: no-store +Set-Cookie: foo2=bar2 + diff --git a/netwerk/test/mochitests/method.sjs b/netwerk/test/mochitests/method.sjs new file mode 100644 index 0000000000..f29b20aa00 --- /dev/null +++ b/netwerk/test/mochitests/method.sjs @@ -0,0 +1,9 @@ +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + + // echo method + response.write(request.method); +} diff --git a/netwerk/test/mochitests/mochitest.ini b/netwerk/test/mochitests/mochitest.ini new file mode 100644 index 0000000000..0f1b40bf2f --- /dev/null +++ b/netwerk/test/mochitests/mochitest.ini @@ -0,0 +1,139 @@ +[DEFAULT] +support-files = + method.sjs + partial_content.sjs + rel_preconnect.sjs + set_cookie_xhr.sjs + reset_cookie_xhr.sjs + web_packaged_app.sjs + file_documentcookie_maxage_chromescript.js + file_loadinfo_redirectchain.sjs + file_1331680.js + file_1503201.sjs + file_iframe_allow_scripts.html + file_iframe_allow_same_origin.html + redirect_idn.html^headers^ + redirect_idn.html + empty.html + redirect.sjs + redirect_to.sjs + origin_header.sjs + origin_header_form_post.html + origin_header_form_post_xorigin.html + subResources.sjs + beltzner.jpg + beltzner.jpg^headers^ + file_chromecommon.js + file_domain_hierarchy_inner.html + file_domain_hierarchy_inner.html^headers^ + file_domain_hierarchy_inner_inner.html + file_domain_hierarchy_inner_inner.html^headers^ + file_domain_hierarchy_inner_inner_inner.html + file_domain_hierarchy_inner_inner_inner.html^headers^ + file_domain_inner.html + file_domain_inner.html^headers^ + file_domain_inner_inner.html + file_domain_inner_inner.html^headers^ + file_image_inner.html + file_image_inner.html^headers^ + file_image_inner_inner.html + file_image_inner_inner.html^headers^ + file_lnk.lnk + file_loadflags_inner.html + file_loadflags_inner.html^headers^ + file_localhost_inner.html + file_localhost_inner.html^headers^ + file_loopback_inner.html + file_loopback_inner.html^headers^ + file_subdomain_inner.html + file_subdomain_inner.html^headers^ + file_testcommon.js + file_testloadflags.js + file_testloadflags_chromescript.js + image1.png + image1.png^headers^ + image2.png + image2.png^headers^ + test1.css + test1.css^headers^ + test2.css + test2.css^headers^ +prefs = + javascript.options.large_arraybuffers=true + +[test_arraybufferinputstream.html] +[test_arraybufferinputstream_large.html] +# Large ArrayBuffers not supported on 32-bit. TSan shadow memory causes OOMs. +skip-if = bits == 32 || tsan || asan +[test_documentcookies_maxage.html] +# Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" +skip-if = xorigin +[test_idn_redirect.html] +skip-if = + http3 +[test_loadinfo_redirectchain.html] +fail-if = xorigin +skip-if = + http3 +[test_partially_cached_content.html] +[test_rel_preconnect.html] +[test_redirect_ref.html] +skip-if = + http3 +[test_uri_scheme.html] +skip-if = (verify && debug && os == 'mac') +[test_viewsource_unlinkable.html] +[test_xhr_method_case.html] +[test_1331680.html] +[test_1331680_iframe.html] +[test_1331680_xhr.html] +skip-if = verify +[test_1396395.html] +skip-if = + http3 +[test_1421324.html] +[test_1425031.html] +[test_1503201.html] +[test_origin_header.html] +skip-if = + http3 +[test_1502055.html] +support-files = sw_1502055.js file_1502055.sjs iframe_1502055.html +[test_accept_header.html] +support-files = test_accept_header.sjs +[test_different_domain_in_hierarchy.html] +skip-if = + http3 +[test_differentdomain.html] +skip-if = + http3 +[test_fetch_lnk.html] +[test_image.html] +skip-if = + http3 +[test_loadflags.html] +# Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" +skip-if = + xorigin + http3 +[test_same_base_domain.html] +skip-if = + http3 +[test_same_base_domain_2.html] +skip-if = + http3 +[test_same_base_domain_3.html] +skip-if = + http3 +[test_same_base_domain_4.html] +skip-if = + http3 +[test_same_base_domain_5.html] +skip-if = + http3 +[test_same_base_domain_6.html] +skip-if = + http3 +[test_samedomain.html] +skip-if = + http3 diff --git a/netwerk/test/mochitests/origin_header.sjs b/netwerk/test/mochitests/origin_header.sjs new file mode 100644 index 0000000000..72cc4b2ae6 --- /dev/null +++ b/netwerk/test/mochitests/origin_header.sjs @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + + var origin = request.hasHeader("Origin") ? request.getHeader("Origin") : ""; + response.write("Origin: " + origin); +} diff --git a/netwerk/test/mochitests/origin_header_form_post.html b/netwerk/test/mochitests/origin_header_form_post.html new file mode 100644 index 0000000000..01c2df5ef2 --- /dev/null +++ b/netwerk/test/mochitests/origin_header_form_post.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <script> + function submitForm() { + document.getElementById("form").submit(); + } + </script> +</head> +<body onload="submitForm()"> + <form action="http://Mochi.test:8888/tests/netwerk/test/mochitests/origin_header.sjs" + method="POST" + id="form"> + <input type="submit" value="Submit POST"> + </form> +</body> +</html> diff --git a/netwerk/test/mochitests/origin_header_form_post_xorigin.html b/netwerk/test/mochitests/origin_header_form_post_xorigin.html new file mode 100644 index 0000000000..52e173747b --- /dev/null +++ b/netwerk/test/mochitests/origin_header_form_post_xorigin.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <script> + function submitForm() { + document.getElementById("form").submit(); + } + </script> +</head> +<body onload="submitForm()"> + <form action="http://test1.mochi.test:8888/tests/netwerk/test/mochitests/origin_header.sjs" + method="POST" + id="form"> + <input type="submit" value="Submit POST"> + </form> +</body> +</html> diff --git a/netwerk/test/mochitests/partial_content.sjs b/netwerk/test/mochitests/partial_content.sjs new file mode 100644 index 0000000000..67bfff377a --- /dev/null +++ b/netwerk/test/mochitests/partial_content.sjs @@ -0,0 +1,193 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Debug and Error wrapper functions for dump(). + */ +function ERR(response, responseCode, responseCodeStr, msg) { + // Reset state var. + setState("expectedRequestType", ""); + // Dump to console log and send to client in response. + dump("SERVER ERROR: " + msg + "\n"); + response.write("HTTP/1.1 " + responseCode + " " + responseCodeStr + "\r\n"); + response.write("Content-Type: text/html; charset=UTF-8\r\n"); + response.write("Content-Length: " + msg.length + "\r\n"); + response.write("\r\n"); + response.write(msg); +} + +function DBG(msg) { + // Dump to console only. + dump("SERVER DEBUG: " + msg + "\n"); +} + +/* Delivers content in parts to test partially cached content: requires two + * requests for the same resource. + * + * First call will respond with partial content, but a 200 header and + * Content-Length equal to the full content length. No Range or If-Range + * headers are allowed in the request. + * + * Second call will require Range and If-Range in the request headers, and + * will respond with the range requested. + */ +function handleRequest(request, response) { + DBG("Trying to seize power"); + response.seizePower(); + + DBG("About to check state vars"); + // Get state var to determine if this is the first or second request. + var expectedRequestType; + var lastModified; + if (getState("expectedRequestType") === "") { + DBG("First call: Should be requesting full content."); + expectedRequestType = "fullRequest"; + // Set state var for second request. + setState("expectedRequestType", "partialRequest"); + // Create lastModified variable for responses. + lastModified = new Date().toUTCString(); + setState("lastModified", lastModified); + } else if (getState("expectedRequestType") === "partialRequest") { + DBG("Second call: Should be requesting undelivered content."); + expectedRequestType = "partialRequest"; + // Reset state var for first request. + setState("expectedRequestType", ""); + // Get last modified date and reset state var. + lastModified = getState("lastModified"); + } else { + ERR( + response, + 500, + "Internal Server Error", + 'Invalid expectedRequestType "' + + expectedRequestType + + '"in ' + + "server state db." + ); + return; + } + + // Look for Range and If-Range + var range = request.hasHeader("Range") ? request.getHeader("Range") : ""; + var ifRange = request.hasHeader("If-Range") + ? request.getHeader("If-Range") + : ""; + + if (expectedRequestType === "fullRequest") { + // Should not have Range or If-Range in first request. + if (range && range.length) { + ERR( + response, + 400, + "Bad Request", + 'Should not receive "Range: ' + range + '" for first, full request.' + ); + return; + } + if (ifRange && ifRange.length) { + ERR( + response, + 400, + "Bad Request", + 'Should not receive "Range: ' + range + '" for first, full request.' + ); + return; + } + } else if (expectedRequestType === "partialRequest") { + // Range AND If-Range should both be present in second request. + if (!range) { + ERR( + response, + 400, + "Bad Request", + 'Should receive "Range: " for second, partial request.' + ); + return; + } + if (!ifRange) { + ERR( + response, + 400, + "Bad Request", + 'Should receive "If-Range: " for second, partial request.' + ); + return; + } + } else { + // Somewhat redundant, but a check for errors in this test code. + ERR( + response, + 500, + "Internal Server Error", + 'expectedRequestType not set correctly: "' + expectedRequestType + '"' + ); + return; + } + + // Prepare content in two parts for responses. + var partialContent = + '<html><head></head><body><p id="firstResponse">First response</p>'; + var remainderContent = + '<p id="secondResponse">Second response</p></body></html>'; + var totalLength = partialContent.length + remainderContent.length; + + DBG("totalLength: " + totalLength); + + // Prepare common headers for the two responses. + let date = new Date(); + DBG("Date: " + date.toUTCString() + ", Last-Modified: " + lastModified); + var commonHeaders = + "Date: " + + date.toUTCString() + + "\r\n" + + "Last-Modified: " + + lastModified + + "\r\n" + + "Content-Type: text/html; charset=UTF-8\r\n" + + "ETag: abcd0123\r\n" + + "Accept-Ranges: bytes\r\n"; + + // Prepare specific headers and content for first and second responses. + if (expectedRequestType === "fullRequest") { + DBG("First response: Sending partial content with a full header"); + response.write("HTTP/1.1 200 OK\r\n"); + response.write(commonHeaders); + // Set Content-Length to full length of resource. + response.write("Content-Length: " + totalLength + "\r\n"); + response.write("\r\n"); + response.write(partialContent); + } else if (expectedRequestType === "partialRequest") { + DBG("Second response: Sending remaining content with a range header"); + response.write("HTTP/1.1 206 Partial Content\r\n"); + response.write(commonHeaders); + // Set Content-Length to length of bytes transmitted. + response.write("Content-Length: " + remainderContent.length + "\r\n"); + response.write( + "Content-Range: bytes " + + partialContent.length + + "-" + + (totalLength - 1) + + "/" + + totalLength + + "\r\n" + ); + response.write("\r\n"); + response.write(remainderContent); + } else { + // Somewhat redundant, but a check for errors in this test code. + ERR( + response, + 500, + "Internal Server Error", + "Something very bad happened here: expectedRequestType is invalid " + + 'towards the end of handleRequest! - "' + + expectedRequestType + + '"' + ); + return; + } + + response.finish(); +} diff --git a/netwerk/test/mochitests/redirect.sjs b/netwerk/test/mochitests/redirect.sjs new file mode 100644 index 0000000000..73efea59cf --- /dev/null +++ b/netwerk/test/mochitests/redirect.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", "empty.html#"); +} diff --git a/netwerk/test/mochitests/redirect_idn.html b/netwerk/test/mochitests/redirect_idn.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/netwerk/test/mochitests/redirect_idn.html diff --git a/netwerk/test/mochitests/redirect_idn.html^headers^ b/netwerk/test/mochitests/redirect_idn.html^headers^ new file mode 100644 index 0000000000..753f65db87 --- /dev/null +++ b/netwerk/test/mochitests/redirect_idn.html^headers^ @@ -0,0 +1,3 @@ +HTTP 301 Moved Permanently +Location: http://exämple.test/tests/netwerk/test/mochitests/empty.html +X-Comment: Bug 1142083 - This is a redirect to http://exämple.test diff --git a/netwerk/test/mochitests/redirect_to.sjs b/netwerk/test/mochitests/redirect_to.sjs new file mode 100644 index 0000000000..f090c70849 --- /dev/null +++ b/netwerk/test/mochitests/redirect_to.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 308, "Permanent Redirect"); + response.setHeader("Location", request.queryString); +} diff --git a/netwerk/test/mochitests/rel_preconnect.sjs b/netwerk/test/mochitests/rel_preconnect.sjs new file mode 100644 index 0000000000..5423c877f0 --- /dev/null +++ b/netwerk/test/mochitests/rel_preconnect.sjs @@ -0,0 +1,17 @@ +// Generate response header "Link: <HREF>; rel=preconnect" +// HREF is provided by the request header X-Link + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader( + "Link", + "<" + + request.queryString + + ">; rel=preconnect" + + ", " + + "<" + + request.queryString + + ">; rel=preconnect; crossOrigin=anonymous" + ); + response.write("check that header"); +} diff --git a/netwerk/test/mochitests/reset_cookie_xhr.sjs b/netwerk/test/mochitests/reset_cookie_xhr.sjs new file mode 100644 index 0000000000..3620958d29 --- /dev/null +++ b/netwerk/test/mochitests/reset_cookie_xhr.sjs @@ -0,0 +1,15 @@ +function handleRequest(request, response) { + var queryString = request.queryString; + switch (queryString) { + case "set_cookie": + response.setHeader("Set-Cookie", "testXHR1=xhr_val1; path=/", false); + break; + case "modify_cookie": + response.setHeader( + "Set-Cookie", + "testXHR1=xhr_val2; path=/; HttpOnly", + false + ); + break; + } +} diff --git a/netwerk/test/mochitests/set_cookie_xhr.sjs b/netwerk/test/mochitests/set_cookie_xhr.sjs new file mode 100644 index 0000000000..19855bfec9 --- /dev/null +++ b/netwerk/test/mochitests/set_cookie_xhr.sjs @@ -0,0 +1,15 @@ +function handleRequest(request, response) { + var queryString = request.queryString; + switch (queryString) { + case "xhr1": + response.setHeader("Set-Cookie", "xhr1=xhr_val1; path=/", false); + break; + case "xhr2": + response.setHeader( + "Set-Cookie", + "xhr2=xhr_val2; path=/; HttpOnly", + false + ); + break; + } +} diff --git a/netwerk/test/mochitests/signed_web_packaged_app.sjs b/netwerk/test/mochitests/signed_web_packaged_app.sjs new file mode 100644 index 0000000000..dc386ad6fe --- /dev/null +++ b/netwerk/test/mochitests/signed_web_packaged_app.sjs @@ -0,0 +1,73 @@ +function handleRequest(request, response) { + response.setHeader("Content-Type", "application/package", false); + response.write(signedPackage); +} + +// The package content +// getData formats it as described at http://www.w3.org/TR/web-packaging/#streamable-package-format +var signedPackage = `manifest-signature: MIIF1AYJKoZIhvcNAQcCoIIFxTCCBcECAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3DQEHAaCCA54wggOaMIICgqADAgECAgEEMA0GCSqGSIb3DQEBCwUAMHMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEkMCIGA1UEChMbRXhhbXBsZSBUcnVzdGVkIENvcnBvcmF0aW9uMRkwFwYDVQQDExBUcnVzdGVkIFZhbGlkIENBMB4XDTE1MTExOTAzMDEwNVoXDTM1MTExOTAzMDEwNVowdDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSQwIgYDVQQKExtFeGFtcGxlIFRydXN0ZWQgQ29ycG9yYXRpb24xGjAYBgNVBAMTEVRydXN0ZWQgQ29ycCBDZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzPback9X7RRxKTc3/5o2vm9Ro6XNiSM9NPsN3djjCIVz50bY0rJkP98zsqpFjnLwqHeJigxyYoqFexRhRLgKrG10CxNl4rxP6CEPENjvj5FfbX/HUZpT/DelNR18F498yD95vSHcSrCc3JrjV3bKA+wgt11E4a0Ba95S1RuwtehZw1+Y4hO8nHpbSGfjD0BpluFY2nDoYAm+aWSrsmLuJsKLO8Xn2I1brZFJUynR3q1ujuDE9EJk1niDLfOeVgXM4AavJS5C0ZBHhAhR2W+K9NN97jpkpmHFqecTwDXB7rEhsyB3185cI7anaaQfHHfH5+4SD+cMDNtYIOSgLO06ZwIDAQABozgwNjAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAQEAlnVyLz5dPhS0ZhZD6qJOUzSo6nFwMxNX1m0oS37mevtuh0b0o1gmEuMw3mVxiAVkC2vPsoxBL2wLlAkcEdBPxGEqhBmtiBY3F3DgvEkf+/sOY1rnr6O1qLZuBAnPzA1Vnco8Jwf0DYF0PxaRd8yT5XSl5qGpM2DItEldZwuKKaL94UEgIeC2c+Uv/IOyrv+EyftX96vcmRwr8ghPFLQ+36H5nuAKEpDD170EvfWl1zs0dUPiqSI6l+hy5V14gl63Woi34L727+FKx8oatbyZtdvbeeOmenfTLifLomnZdx+3WMLkp3TLlHa5xDLwifvZtBP2d3c6zHp7gdrGU1u2WTGCAf4wggH6AgEBMHgwczELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSQwIgYDVQQKExtFeGFtcGxlIFRydXN0ZWQgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFRydXN0ZWQgVmFsaWQgQ0ECAQQwCQYFKw4DAhoFAKBdMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTE1MTEyNTAzMDQzMFowIwYJKoZIhvcNAQkEMRYEFD4ut4oKoYdcGzyfQE6ROeazv+uNMA0GCSqGSIb3DQEBAQUABIIBAFG99dKBSOzQmYVn6lHKWERVDtYXbDTIVF957ID8YH9B5unlX/PdludTNbP5dzn8GWQV08tNRgoXQ5sgxjifHunrpaR1WiR6XqvwOCBeA5NB688jxGNxth6zg6fCGFaynsYMX3FlglfIW+AYwyQUclbv+C4UORJpBjvuknOnK+UDBLVSoP9ivL6KhylYna3oFcs0SMsumc/jf/oQW51LzFHpn61TRUqdDgvGhwcjgphMhKj23KwkjwRspU2oIWNRAuhZgqDD5BJlNniCr9X5Hx1dW6tIVISO91CLAryYkGZKRJYekXctCpIvldUkIDeh2tAw5owr0jtsVd6ovFF3bV4=\r +--NKWXJUAFXB\r +Content-Location: manifest.webapp\r +Content-Type: application/x-web-app-manifest+json\r +\r +{ + "moz-package-origin": "http://mochi.test:8888", + "name": "My App", + "moz-resources": [ + { + "src": "page2.html", + "integrity": "JREF3JbXGvZ+I1KHtoz3f46ZkeIPrvXtG4VyFQrJ7II=" + }, + { + "src": "index.html", + "integrity": "Jkvco7U8WOY9s0YREsPouX+DWK7FWlgZwA0iYYSrb7Q=" + }, + { + "src": "scripts/script.js", + "integrity": "6TqtNArQKrrsXEQWu3D9ZD8xvDRIkhyV6zVdTcmsT5Q=" + }, + { + "src": "scripts/library.js", + "integrity": "TN2ByXZiaBiBCvS4MeZ02UyNi44vED+KjdjLInUl4o8=" + } + ], + "moz-permissions": [ + { + "systemXHR": { + "description": "Needed to download stuff" + } + } + ], + "package-identifier": "09bc9714-7ab6-4320-9d20-fde4c237522c", + "description": "A great app!" +}\r +--NKWXJUAFXB\r +Content-Location: page2.html\r +Content-Type: text/html\r +\r +<html> + page2.html +</html> +\r +--NKWXJUAFXB\r +Content-Location: index.html\r +Content-Type: text/html\r +\r +<html> + Last updated: 2015/10/28 + <iframe id="innerFrame" src="page2.html"></iframe> +</html> +\r +--NKWXJUAFXB\r +Content-Location: scripts/script.js\r +Content-Type: text/javascript\r +\r +// script.js +\r +--NKWXJUAFXB\r +Content-Location: scripts/library.js\r +Content-Type: text/javascript\r +\r +// library.js +\r +--NKWXJUAFXB--`; diff --git a/netwerk/test/mochitests/subResources.sjs b/netwerk/test/mochitests/subResources.sjs new file mode 100644 index 0000000000..ec2cfaa750 --- /dev/null +++ b/netwerk/test/mochitests/subResources.sjs @@ -0,0 +1,78 @@ +const kTwoDays = 2 * 24 * 60 * 60; +const kInTwoDays = new Date().getTime() + kTwoDays * 1000; + +function getDateInTwoDays() { + let date2 = new Date(kInTwoDays); + let days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + let months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + let day = date2.getUTCDate(); + if (day < 10) { + day = "0" + day; + } + let month = months[date2.getUTCMonth()]; + let year = date2.getUTCFullYear(); + let hour = date2.getUTCHours(); + if (hour < 10) { + hour = "0" + hour; + } + let minute = date2.getUTCMinutes(); + if (minute < 10) { + minute = "0" + minute; + } + let second = date2.getUTCSeconds(); + if (second < 10) { + second = "0" + second; + } + return ( + days[date2.getUTCDay()] + + ", " + + day + + "-" + + month + + "-" + + year + + " " + + hour + + ":" + + minute + + ":" + + second + + " GMT" + ); +} + +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + let suffix = " path=/; domain:.mochi.test"; + + if (aRequest.queryString.includes("3")) { + aResponse.setHeader( + "Set-Cookie", + "test3=value3; expires=Fri, 02-Jan-2037 00:00:01 GMT;" + suffix + ); + } else if (aRequest.queryString.includes("4")) { + let date2 = getDateInTwoDays(); + + aResponse.setHeader( + "Set-Cookie", + "test4=value4; expires=" + date2 + ";" + suffix + ); + } + + aResponse.setHeader("Content-Type", "text/javascript", false); + aResponse.write("42;"); +} diff --git a/netwerk/test/mochitests/sw_1502055.js b/netwerk/test/mochitests/sw_1502055.js new file mode 100644 index 0000000000..40a8c178f1 --- /dev/null +++ b/netwerk/test/mochitests/sw_1502055.js @@ -0,0 +1 @@ +/* empty */ diff --git a/netwerk/test/mochitests/test1.css b/netwerk/test/mochitests/test1.css new file mode 100644 index 0000000000..139597f9cb --- /dev/null +++ b/netwerk/test/mochitests/test1.css @@ -0,0 +1,2 @@ + + diff --git a/netwerk/test/mochitests/test1.css^headers^ b/netwerk/test/mochitests/test1.css^headers^ new file mode 100644 index 0000000000..729babb5a4 --- /dev/null +++ b/netwerk/test/mochitests/test1.css^headers^ @@ -0,0 +1,3 @@ +Cache-Control: no-cache +Set-Cookie: css=bar + diff --git a/netwerk/test/mochitests/test2.css b/netwerk/test/mochitests/test2.css new file mode 100644 index 0000000000..139597f9cb --- /dev/null +++ b/netwerk/test/mochitests/test2.css @@ -0,0 +1,2 @@ + + diff --git a/netwerk/test/mochitests/test2.css^headers^ b/netwerk/test/mochitests/test2.css^headers^ new file mode 100644 index 0000000000..b12d32c72b --- /dev/null +++ b/netwerk/test/mochitests/test2.css^headers^ @@ -0,0 +1,3 @@ +Cache-Control: no-cache +Set-Cookie: css2=bar2 + diff --git a/netwerk/test/mochitests/test_1331680.html b/netwerk/test/mochitests/test_1331680.html new file mode 100644 index 0000000000..30d74de0af --- /dev/null +++ b/netwerk/test/mochitests/test_1331680.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1331680 +--> +<head> + <title>Cookies set in content processes update immediately.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1331680">Mozilla Bug 1331680</a> +<p id="display"></p> +<div id="content" style="display: none"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('file_1331680.js')); + +// Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" +SpecialPowers.pushPrefEnv({ + "set": [ + ["network.cookie.sameSite.laxByDefault", false], + ] +}, () => { + gScript.addMessageListener("cookieName", confirmCookieName); + gScript.addMessageListener("createObserver:return", testSetCookie); + gScript.addMessageListener("removeObserver:return", finishTest); + gScript.sendAsyncMessage('createObserver'); +}); + +var testsNum = 0; + +function confirmRemoveAllCookies() { + is(document.cookie, "", "Removed all cookies."); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); +} + +// Confirm the notify which represents the cookie is updating. +function confirmCookieName(name) { + testsNum++; + switch(testsNum) { + case 1: + case 3: + is(name, "cookie0=test1", "An update for the cookie named " + name + " was observed."); + break; + case 2: + is(name, "cookie2=test3", "An update for the cookie named " + name + " was observed."); + break; + case 4: + is(name, "cookie2=test3", "An update for the cookie named " + name + " was observed."); + gScript.sendAsyncMessage('removeObserver'); + break; + } +} + +function finishTest() { + is(document.cookie, "", "Removed all cookies from cookie-changed"); + SimpleTest.finish(); +} + +/* Test document.cookie + * 1. Set a cookie and confirm the cookies which are processed from observer. + * 2. Set a cookie and get cookie. + */ +const COOKIE_NAMES = ["cookie0", "cookie1", "cookie2"]; +function testSetCookie() { + document.cookie = COOKIE_NAMES[0] + "=test1"; + document.cookie = COOKIE_NAMES[1] + "=test2; HttpOnly"; + document.cookie = COOKIE_NAMES[2] + "=test3"; + var confirmCookieString = COOKIE_NAMES[0] + "=test1; " + COOKIE_NAMES[2] + "=test3"; + is(document.cookie, confirmCookieString, "Confirm the cookies string which be get is current."); + for (var i = 0; i < COOKIE_NAMES.length; i++) { + document.cookie = COOKIE_NAMES[i] + "=; expires=Thu, 01-Jan-1970 00:00:01 GMT;"; + } + is(document.cookie, "", "Removed all cookies."); +} + +</script> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_1331680_iframe.html b/netwerk/test/mochitests/test_1331680_iframe.html new file mode 100644 index 0000000000..85842332b1 --- /dev/null +++ b/netwerk/test/mochitests/test_1331680_iframe.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=643051 +--> +<head> + <title>Cookies set from iframe in content process</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1331680">Mozilla Bug 1331680</a> +<p id="display"></p> +<div id="content" style="display: none"> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +const IFRAME_COOKIE_NAMES = ["if1", "if2_1", "if2_2"]; +const ID = ["if_1", "if_2", "if_3"]; + +var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('file_1331680.js')); + +/* Test iframe + * 1. Create three iframes, and one of the iframe will create two cookies. + * 2. Confirm the cookies can be proessed from observer. + * 3. Confirm document.cookie can get cookies "if2_1" and "if2_2". + * 4. Confirm the iframe whose source is "about:blank" can get parent's cookies. + */ +function createIframe(id, src, sandbox_flags) { + return new Promise(resolve => { + var ifr = document.createElement("iframe"); + ifr.id = id; + ifr.src = src; + ifr.sandbox = sandbox_flags; + ifr.addEventListener("load", resolve); + document.body.appendChild(ifr); + }); +}; + +function confirmCookies(id) { + is(document.cookie, "if2_1=if2_val1; if2_2=if2_val2", "Confirm the cookies can get after iframe was deleted"); + var new_ifr = document.getElementById(id); + is(new_ifr.contentDocument.cookie, document.cookie, "Confirm the inner document.cookie = parent document.cookie"); + document.cookie = IFRAME_COOKIE_NAMES[1] + "=; expires=Thu, 01-Jan-1970 00:00:01 GMT"; + document.cookie = IFRAME_COOKIE_NAMES[2] + "=; expires=Thu, 01-Jan-1970 00:00:01 GMT"; + is(document.cookie, "", "Removed all cookies"); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); +} + +addEventListener("message", function(event) { + is(event.data, document.cookie, "Confirm the iframe 2 can communicate with iframe"); +}); + +SpecialPowers.pushPrefEnv({ + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + set: [["network.cookie.sameSite.laxByDefault", false]], +}).then(_ => createIframe(ID[0], "file_iframe_allow_scripts.html", "allow-scripts")) + .then(_ => createIframe(ID[1], "file_iframe_allow_same_origin.html", "allow-scripts allow-same-origin")) + .then(_ => createIframe(ID[2], "about:blank", "allow-scripts allow-same-origin")) + .then(_ => confirmCookies(ID[2])); + +</script> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_1331680_xhr.html b/netwerk/test/mochitests/test_1331680_xhr.html new file mode 100644 index 0000000000..0649d33ff4 --- /dev/null +++ b/netwerk/test/mochitests/test_1331680_xhr.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>Cookie changes from XHR requests are observed in content processes.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +const XHR_COOKIE_NAMES = ["xhr1", "xhr2"]; + +var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('file_1331680.js')); +gScript.addMessageListener("cookieName", confirmCookieName); +gScript.addMessageListener("removeObserver:return", finishTest); +gScript.sendAsyncMessage('createObserver'); + +// Confirm the notify which represents the cookie is updating. +var testsNum = 0; +function confirmCookieName(name) { + testsNum++; + switch(testsNum) { + case 1: + is(name, "xhr1=xhr_val1", "The cookie which names " + name + " is update to db"); + break; + case 2: + is(document.cookie, "xhr1=xhr_val1", "Confirm the cookie string"); + for (var i = 0; i < XHR_COOKIE_NAMES.length; i++) { + document.cookie = XHR_COOKIE_NAMES[i] + "=; path=/; expires=Thu, 01-Jan-1970 00:00:01 GMT"; + } + break; + case 3: + is(document.cookie, "", "Confirm the cookie string"); + gScript.sendAsyncMessage('removeObserver'); + break; + } +} + +function finishTest() { + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); +} + +function createXHR(url) { + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); // async request + xhr.onload = function () { + if (this.status >= 200 && this.status < 300) { + resolve(xhr.response); + } else { + reject({ + status: this.status, + statusText: xhr.statusText + }); + } + }; + xhr.onerror = function () { + reject({ + status: this.status, + statusText: xhr.statusText + }); + }; + xhr.send(); + }); +} + +/* Test XHR + * 1. Create two XHR. + * 2. One of the XHR create a cookie names "xhr1", and other one create a http-only cookie names "xhr2". + * 3. Child process only set xhr1 to cookies hash table. + * 4. Child process only can get the xhr1 cookie from cookies hash table. + */ +SpecialPowers.pushPrefEnv({ + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + set: [["network.cookie.sameSite.laxByDefault", false]], +}).then(_ => createXHR('set_cookie_xhr.sjs?xhr1')) + .then(_ => createXHR('set_cookie_xhr.sjs?xhr2')); + +</script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_1396395.html b/netwerk/test/mochitests/test_1396395.html new file mode 100644 index 0000000000..dcfb952b3a --- /dev/null +++ b/netwerk/test/mochitests/test_1396395.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: --> +<html> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> + <iframe id="f" src="about:blank"></iframe> + <script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var script = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + Services.obs.addObserver(function onExamResp(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (!channel.URI.spec.startsWith("http://example.org")) { + return; + } + Services.obs.removeObserver(onExamResp, 'http-on-examine-response'); + channel.suspend(); + Promise.resolve().then(() => { + channel.resume(); + }); + }, 'http-on-examine-response'); + + sendAsyncMessage('start-test'); +}); + +script.addMessageListener('start-test', () => { + const iframe = document.getElementById('f'); + + iframe.contentWindow.onunload = function () { + info('initiate sync XHR during the page loading'); + let xhr = new XMLHttpRequest(); + xhr.open('GET', window.location, false); + xhr.send(null); + ok(true, 'complete without crash'); + script.destroy(); + SimpleTest.finish(); + } + + iframe.src = 'http://example.org'; +}); + </script> +</body> +</html> diff --git a/netwerk/test/mochitests/test_1421324.html b/netwerk/test/mochitests/test_1421324.html new file mode 100644 index 0000000000..627a339e0a --- /dev/null +++ b/netwerk/test/mochitests/test_1421324.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>Cookie changes from XHR requests are observed in content processes.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('file_1331680.js')); +gScript.addMessageListener("cookieName", confirmCookieName); +gScript.addMessageListener("removeObserver:return", finishTest); +gScript.sendAsyncMessage('createObserver'); + +// Confirm the notify which represents the cookie is updating. +var testsNum = 0; +function confirmCookieName(name) { + testsNum++; + switch(testsNum) { + case 1: + is(name, "testXHR1=xhr_val1", "The cookie which names " + name + " is update to db"); + break; + case 2: + document.cookie = "testXHR2=xhr_val2; path=/"; + break; + case 3: + is(document.cookie, "testXHR2=xhr_val2", "Confirm the cookie string"); + document.cookie = "testXHR1=; path=/; expires=Thu, 01-Jan-1970 00:00:01 GMT"; + document.cookie = "testXHR2=; path=/; expires=Thu, 01-Jan-1970 00:00:01 GMT"; + gScript.sendAsyncMessage('removeObserver'); + break; + } +} + +function finishTest() { + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); +} + +function createXHR(url) { + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); // async request + xhr.onload = function () { + if (this.status >= 200 && this.status < 300) { + resolve(xhr.response); + } else { + reject({ + status: this.status, + statusText: xhr.statusText + }); + } + }; + xhr.onerror = function () { + reject({ + status: this.status, + statusText: xhr.statusText + }); + }; + xhr.send(); + }); +} + + +/* Test XHR + * 1. Create two XHR. + * 2. One of the XHR create a cookie names "set_cookie", and other one create a http-only cookie names "modify_cookie". + * 3. Child process only set testXHR1 to cookies hash table. + * 4. Child process only can get the testXHR1 cookie from cookies hash table. + */ +SpecialPowers.pushPrefEnv({ + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + set: [["network.cookie.sameSite.laxByDefault", false]], +}).then(_ => createXHR('reset_cookie_xhr.sjs?set_cookie')) + .then(_ => createXHR('reset_cookie_xhr.sjs?modify_cookie')); + +</script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_1425031.html b/netwerk/test/mochitests/test_1425031.html new file mode 100644 index 0000000000..25fbfc827c --- /dev/null +++ b/netwerk/test/mochitests/test_1425031.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1425031 +--> +<head> + <title>Cookies set in content processes update immediately.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1425031">Mozilla Bug 1425031</a> +<p id="display"></p> +<div id="content" style="display: none"> +<script type="application/javascript"> + +// Verify that cookie operations initiated by content processes do not cause +// asynchronous updates for those operations to be processed later. + +SimpleTest.waitForExplicitFinish(); + +var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('file_1331680.js')); +var testsNum = 0; +var cookieString = "cookie0=test"; + +// Confirm the notify which represents the cookie is updating. +function confirmCookieOperation(op) { + testsNum++; + switch(testsNum) { + case 1: + is(op, "added", "Confirm the cookie operation is added."); + is(document.cookie, cookieString, "Confirm the cookie string is unaffected by the addition"); + break; + case 2: + is(op, "deleted", "Confirm the cookie operation is deleted."); + is(document.cookie, cookieString, "Confirm the cookie string is unaffected by the deletion"); + break; + case 3: + is(op, "added", "Confirm the cookie operation is added."); + is(document.cookie, cookieString, "Confirm the cookie string is unaffected by the second addition."); + document.cookie = "cookie0=; expires=Thu, 01-Jan-1970 00:00:01 GMT;"; + gScript.sendAsyncMessage('removeObserver'); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); + break; + } +} + +function testSetCookie() { + document.cookie = cookieString; + is(document.cookie, cookieString, "Confirm cookie string."); + document.cookie = "cookie0=; expires=Thu, 01-Jan-1970 00:00:01 GMT;"; + is(document.cookie, "", "Removed all cookies."); + document.cookie = cookieString; + is(document.cookie, cookieString, "Confirm cookie string."); +} + +// Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" +SpecialPowers.pushPrefEnv({ + set: [["network.cookie.sameSite.laxByDefault", false]], +}, () => { + gScript.addMessageListener("cookieOperation", confirmCookieOperation); + gScript.addMessageListener("createObserver:return", testSetCookie); + gScript.sendAsyncMessage('createObserver'); +}); + +</script> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_1502055.html b/netwerk/test/mochitests/test_1502055.html new file mode 100644 index 0000000000..472ee204e6 --- /dev/null +++ b/netwerk/test/mochitests/test_1502055.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Clear-Site-Data + 304 header.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + (async () => { + // Grant example.org first-party storage-access to allow service workers to + // run in the third-party context when dFPI is enabled. This won't be + // necessary anymore once we enable service worker partitioning in beta and + // release. See Bug 1730885. + await SpecialPowers.pushPermissions([ + { + type: "3rdPartyStorage^https://example.org", + allow: true, + context: document.location.origin, + }, + ]); + await SpecialPowers.pushPrefEnv({ + "set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ] + }) + let ifr = document.createElement('iframe'); + ifr.src = "https://example.org/tests/netwerk/test/mochitests/iframe_1502055.html"; + document.body.appendChild(ifr); + addEventListener("message", e => { + if (e.data.type == "finish") { + ok(true, "Test passed"); + SimpleTest.finish(); + return; + } + + if (e.data.type == "info") { + info(e.data.msg); + } + }); + })(); +</script> + +</body> +</html> diff --git a/netwerk/test/mochitests/test_1503201.html b/netwerk/test/mochitests/test_1503201.html new file mode 100644 index 0000000000..2e724f33ce --- /dev/null +++ b/netwerk/test/mochitests/test_1503201.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1503201 +--> +<head> + <title>A WWW-Authenticate response header with an invalid realm doesn't crash the browser</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1503201">Mozilla Bug 1503201</a> +<p id="display"></p> +<div id="content" style="display: none"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +fetch("file_1503201.sjs") + .then(() => ok(true, "no crash")) + .then(() => SimpleTest.finish()); + +</script> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_accept_header.html b/netwerk/test/mochitests/test_accept_header.html new file mode 100644 index 0000000000..0acae2a825 --- /dev/null +++ b/netwerk/test/mochitests/test_accept_header.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Accept header</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script> +SimpleTest.requestCompleteLog(); + +// All the requests are sent to test_accept_header.sjs which will return +// different content based on the queryString. When the queryString is 'get', +// test_accept_header.sjs returns a JSON object with the latest request and its +// accept header value. + +function test_last_request_and_continue(query, expected) { + fetch("test_accept_header.sjs?get").then(r => r.json()).then(json => { + is(json.type, query, "Expected: " + query); + is(json.accept, expected, "Accept header: " + expected); + next(); + }); +} + +function test_iframe() { + let ifr = document.createElement("iframe"); + ifr.src = "test_accept_header.sjs?iframe"; + ifr.onload = () => { + test_last_request_and_continue("iframe", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"); + }; + document.body.appendChild(ifr); +} + +function test_image() { + let i = new Image(); + i.src = "test_accept_header.sjs?image"; + i.onload = function() { + // Fetch spec says we should have: "image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5" + test_last_request_and_continue("image", "image/avif,image/webp,*/*"); + } +} + +function test_style() { + let head = document.getElementsByTagName("head")[0]; + let link = document.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = "test_accept_header.sjs?style"; + link.onload = () => { + test_last_request_and_continue("style", "text/css,*/*;q=0.1"); + }; + head.appendChild(link); +} + +function test_worker() { + let w = new Worker("test_accept_header.sjs?worker"); + w.onmessage = function() { + test_last_request_and_continue("worker", "*/*"); + } +} + +let tests = [ + test_iframe, + test_image, + test_style, + test_worker, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + let test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv({ "set": [ + [ "dom.enable_performance_observer", true ] +]}, next); + +</script> +</body> +</html> diff --git a/netwerk/test/mochitests/test_accept_header.sjs b/netwerk/test/mochitests/test_accept_header.sjs new file mode 100644 index 0000000000..6e73acd293 --- /dev/null +++ b/netwerk/test/mochitests/test_accept_header.sjs @@ -0,0 +1,62 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, "200", "OK"); + dump(`test_accept_header ${request.path}?${request.queryString}\n`); + + if (request.queryString == "worker") { + response.setHeader("Content-Type", "text/javascript", false); + response.write("postMessage(42)"); + + setState( + "data", + JSON.stringify({ type: "worker", accept: request.getHeader("Accept") }) + ); + return; + } + + if (request.queryString == "image") { + // A 1x1 PNG image. + // Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) + const IMAGE = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" + ); + + response.setHeader("Content-Type", "image/png", false); + response.write(IMAGE); + + setState( + "data", + JSON.stringify({ type: "image", accept: request.getHeader("Accept") }) + ); + return; + } + + if (request.queryString == "style") { + response.setHeader("Content-Type", "text/css", false); + response.write(""); + + setState( + "data", + JSON.stringify({ type: "style", accept: request.getHeader("Accept") }) + ); + return; + } + + if (request.queryString == "iframe") { + response.setHeader("Content-Type", "text/html", false); + response.write("<h1>Hello world!</h1>"); + + setState( + "data", + JSON.stringify({ type: "iframe", accept: request.getHeader("Accept") }) + ); + return; + } + + if (request.queryString == "get") { + response.setHeader("Content-Type", "application/json", false); + response.write(getState("data")); + + setState("data", ""); + } +} diff --git a/netwerk/test/mochitests/test_arraybufferinputstream.html b/netwerk/test/mochitests/test_arraybufferinputstream.html new file mode 100644 index 0000000000..5e3c5faa3e --- /dev/null +++ b/netwerk/test/mochitests/test_arraybufferinputstream.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>ArrayBuffer stream test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> +function detachArrayBuffer(ab) +{ + var w = new Worker("data:application/javascript,"); + w.postMessage(ab, [ab]); +} + +function test() +{ + var ab = new ArrayBuffer(4000); + var ta = new Uint8Array(ab); + ta[0] = 'a'.charCodeAt(0); + ta[1] = 'b'.charCodeAt(0); + + const Cc = SpecialPowers.Cc, Ci = SpecialPowers.Ci; + var abis = Cc["@mozilla.org/io/arraybuffer-input-stream;1"] + .createInstance(Ci.nsIArrayBufferInputStream); + + var sis = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + sis.init(abis); + + is(sis.read(1), "", "should read no data from an uninitialized ABIS"); + + abis.setData(ab, 0, 256 * 1024); + + is(sis.read(1), "a", "should read 'a' after init"); + + detachArrayBuffer(ab); + + SpecialPowers.forceGC(); + SpecialPowers.forceGC(); + + try + { + is(sis.read(1), "b", "should read 'b' after detaching buffer"); + } + catch (e) + { + ok(false, "reading from stream should have worked"); + } + + // A regression test for bug 1265076. Previously, overflowing + // the internal buffer from readSegments would cause incorrect + // copying. The constant mirrors the value in + // ArrayBufferInputStream::readSegments. + var size = 8192; + ab = new ArrayBuffer(2 * size); + ta = new Uint8Array(ab); + + var i; + for (i = 0; i < size; ++i) { + ta[i] = 'x'.charCodeAt(0); + } + for (i = 0; i < size; ++i) { + ta[size + i] = 'y'.charCodeAt(0); + } + + abis = Cc["@mozilla.org/io/arraybuffer-input-stream;1"] + .createInstance(Ci.nsIArrayBufferInputStream); + abis.setData(ab, 0, 2 * size); + + sis = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + sis.init(abis); + + var result = sis.read(2 * size); + is(result, "x".repeat(size) + "y".repeat(size), "correctly read the data"); +} + +test(); +</script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_arraybufferinputstream_large.html b/netwerk/test/mochitests/test_arraybufferinputstream_large.html new file mode 100644 index 0000000000..33922d6e37 --- /dev/null +++ b/netwerk/test/mochitests/test_arraybufferinputstream_large.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>ArrayBuffer stream with large ArrayBuffer test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> +add_task(async function testLargeArrayBuffer() { + let ab = new ArrayBuffer(4.5 * 1024 * 1024 * 1024); // 4.5 GB. + let ta = new Uint8Array(ab); + + const { Cc, Ci } = SpecialPowers; + let abis = Cc["@mozilla.org/io/arraybuffer-input-stream;1"] + .createInstance(Ci.nsIArrayBufferInputStream); + + let sis = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + sis.init(abis); + + // The stream currently doesn't support more than UINT32_MAX bytes. + let ex; + try { + abis.setData(ab, 0, ab.byteLength); + } catch (e) { + ex = e; + } + is(ex.message.includes("NS_ERROR_ILLEGAL_VALUE"), true, "Expecting exception"); + + // Reading a small slice of the large ArrayBuffer is fine, even near the end. + ta[ta.length - 10] = "a".charCodeAt(0); + ta[ta.length - 9] = "b".charCodeAt(0); + ta[ta.length - 8] = "c".charCodeAt(0); + abis.setData(ab, ab.byteLength - 10, 2); + is(sis.read(1), "a", "should read 'a' after init"); + is(sis.read(1), "b", "should read 'b' after 'a'"); + is(sis.read(1), "", "Should be done reading data"); +}); +</script> + +</head> +<body> +</body> +</html> diff --git a/netwerk/test/mochitests/test_different_domain_in_hierarchy.html b/netwerk/test/mochitests/test_different_domain_in_hierarchy.html new file mode 100644 index 0000000000..0ec6d35d4d --- /dev/null +++ b/netwerk/test/mochitests/test_different_domain_in_hierarchy.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test cookie requests from within a window hierarchy of different base domains</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="setupTest('https://example.org/tests/netwerk/test/mochitests/file_domain_hierarchy_inner.html', 4, 3)"> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="text/javascript" src="file_testcommon.js"> +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_differentdomain.html b/netwerk/test/mochitests/test_differentdomain.html new file mode 100644 index 0000000000..75cc903758 --- /dev/null +++ b/netwerk/test/mochitests/test_differentdomain.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Cross domain access to properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="setupTest('https://example.com/tests/netwerk/test/mochitests/file_domain_inner.html', 3, 2)"> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="text/javascript" src="file_testcommon.js"> +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_documentcookies_maxage.html b/netwerk/test/mochitests/test_documentcookies_maxage.html new file mode 100644 index 0000000000..eb130b2fed --- /dev/null +++ b/netwerk/test/mochitests/test_documentcookies_maxage.html @@ -0,0 +1,149 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>Test for document.cookie max-age pref</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> +const kTwoDays = 2 * 24 * 60 * 60; +const kSevenDays = 7 * 24 * 60 * 60; +const kInTwoDays = (new Date().getTime() + kTwoDays * 1000); +const kInSevenDays = (new Date().getTime() + kSevenDays * 1000); +const kScriptURL = SimpleTest.getTestFileURL("file_documentcookie_maxage_chromescript.js"); + +let gScript; + +function getDateInTwoDays() +{ + let date2 = new Date(kInTwoDays); + let days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", + "Nov", "Dec"]; + let day = date2.getUTCDate(); + if (day < 10) { + day = "0" + day; + } + let month = months[date2.getUTCMonth()]; + let year = date2.getUTCFullYear(); + let hour = date2.getUTCHours(); + if (hour < 10) { + hour = "0" + hour; + } + let minute = date2.getUTCMinutes(); + if (minute < 10) { + minute = "0" + minute; + } + let second = date2.getUTCSeconds(); + if (second < 10) { + second = "0" + second; + } + return days[date2.getUTCDay()] + ", " + day + "-" + month + "-" + + year + " " + hour + ":" + minute + ":" + second + " GMT"; +} + +function dotest() +{ + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({ + set: [["privacy.documentCookies.maxage", kSevenDays]], + }).then(_ => { + gScript = SpecialPowers.loadChromeScript(kScriptURL); + + return new Promise(resolve => { + gScript.addMessageListener("init:return", resolve); + gScript.sendAsyncMessage("init"); + }); + }).then(_ => { + let date2 = getDateInTwoDays(); + + document.cookie = "test1=value1; expires=Fri, 02-Jan-2037 00:00:01 GMT;"; + document.cookie = "test2=value2; expires=" + date2 + ";"; + + return fetch("subResources.sjs?3"); + }).then(_ => { + return fetch("subResources.sjs?4"); + }).then(_ => { + return new Promise(resolve => { + gScript.addMessageListener("getCookies:return", resolve); + gScript.sendAsyncMessage("getCookies"); + }); + }).then(_ => { + for (let cookie of _.cookies) { + switch (cookie.name) { + case "test1": { + is(cookie.value, "value1", "The correct value expected"); + let d = new Date(cookie.expires * 1000); + let [day, month, year] = [d.getUTCDate(), d.getUTCMonth(), d.getUTCFullYear()]; + let d2 = new Date(kInSevenDays); + let [day2, month2, year2] = [d2.getUTCDate(), d2.getUTCMonth(), d2.getUTCFullYear()]; + is(day, day2, "Days match"); + is(month, month2, "Months match"); + is(year, year2, "Years match"); + } + break; + + case "test2": { + is(cookie.value, "value2", "The correct value expected"); + let d = new Date(cookie.expires * 1000); + let [day, month, year] = [d.getUTCDate(), d.getUTCMonth(), d.getUTCFullYear()]; + let d2 = new Date(kInTwoDays); + let [day2, month2, year2] = [d2.getUTCDate(), d2.getUTCMonth(), d2.getUTCFullYear()]; + is(day, day2, "Days match"); + is(month, month2, "Months match"); + is(year, year2, "Years match"); + } + break; + + case "test3": { + is(cookie.value, "value3", "The correct value expected"); + let d = new Date(cookie.expires * 1000); + let [day, month, year] = [d.getUTCDate(), d.getUTCMonth(), d.getUTCFullYear()]; + let d2 = new Date("Fri, 02 Jan 2037 00:00:01 GMT"); + let [day2, month2, year2] = [d2.getUTCDate(), d2.getUTCMonth(), d2.getUTCFullYear()]; + is(day, day2, "Days match"); + is(month, month2, "Months match"); + is(year, year2, "Years match"); + } + break; + + case "test4": { + is(cookie.value, "value4", "The correct value expected"); + let d = new Date(cookie.expires * 1000); + let [day, month, year] = [d.getUTCDate(), d.getUTCMonth(), d.getUTCFullYear()]; + let d2 = new Date(kInTwoDays); + let [day2, month2, year2] = [d2.getUTCDate(), d2.getUTCMonth(), d2.getUTCFullYear()]; + is(day, day2, "Days match"); + is(month, month2, "Months match"); + is(year, year2, "Years match"); + } + break; + + default: + ok(false, "Unexpected cookie found!"); + break; + } + } + + return new Promise(resolve => { + gScript.addMessageListener("shutdown:return", resolve); + gScript.sendAsyncMessage("shutdown"); + }); + }).then(finish); +} + +function finish() +{ + SimpleTest.finish(); +} +</script> +</head> +<body onload="dotest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_fetch_lnk.html b/netwerk/test/mochitests/test_fetch_lnk.html new file mode 100644 index 0000000000..e1154a5951 --- /dev/null +++ b/netwerk/test/mochitests/test_fetch_lnk.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Downloading .lnk through HTTP should always download the file without parsing it</title> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script> + SimpleTest.waitForExplicitFinish(); + // Download .lnk which points to a system executable + fetch("file_lnk.lnk").then(async res => { + ok(res.ok, "Download success"); + ok(res.url.endsWith("file_lnk.lnk"), "file name should be of the lnk file"); + is(res.headers.get("Content-Length"), "1531", "The size should be of the lnk file"); + SimpleTest.finish(); + }, () => { + ok(false, "Unreachable code"); + }) +</script> diff --git a/netwerk/test/mochitests/test_idn_redirect.html b/netwerk/test/mochitests/test_idn_redirect.html new file mode 100644 index 0000000000..cc920665b3 --- /dev/null +++ b/netwerk/test/mochitests/test_idn_redirect.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- + Bug 1142083 - IDN Unicode domain redirect is broken + This test loads redirectme.html which is redirected simple_test.html, on a different IDN domain. + A message is posted to that page, with responds with another. + Upon receiving that message, we consider that the IDN redirect has functioned properly, since the intended page was loaded. +--> +<head> + <title>Test for URI Manipulation</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var iframe = document.createElement("iframe"); +iframe.src = "about:blank"; +iframe.addEventListener("load", finishTest); +document.body.appendChild(iframe); +iframe.src = "http://mochi.test:8888/tests/netwerk/test/mochitests/redirect_idn.html"; + +function finishTest(e) { + ok(true); + SimpleTest.finish(); +} + +</script> + +</body> +</html> diff --git a/netwerk/test/mochitests/test_image.html b/netwerk/test/mochitests/test_image.html new file mode 100644 index 0000000000..db75cdbca5 --- /dev/null +++ b/netwerk/test/mochitests/test_image.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Cross domain access to properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="setupTest('https://example.org/tests/netwerk/test/mochitests/file_image_inner.html', 7, 3)"> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="text/javascript" src="file_testcommon.js"></script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_loadflags.html b/netwerk/test/mochitests/test_loadflags.html new file mode 100644 index 0000000000..fba93102ea --- /dev/null +++ b/netwerk/test/mochitests/test_loadflags.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Cross domain access to properties</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<!-- + *5 cookies: 1+1 from file_testloadflags.js, 2 from file_loadflags_inner.html + 1 from beltzner.jpg. + *1 load: file_loadflags_inner.html. + *2 headers: 1 for file_loadflags_inner.html + 1 for beltzner.jpg. + --> +<body onload="setupTest('http://example.org/tests/netwerk/test/mochitests/file_loadflags_inner.html', 'example.org', 5, 2, 2)"> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="text/javascript" src="file_testloadflags.js"> +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_loadinfo_redirectchain.html b/netwerk/test/mochitests/test_loadinfo_redirectchain.html new file mode 100644 index 0000000000..a657ce678c --- /dev/null +++ b/netwerk/test/mochitests/test_loadinfo_redirectchain.html @@ -0,0 +1,269 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1194052 - Append Principal to RedirectChain within LoadInfo before the channel is succesfully openend</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* + * We perform the following tests on the redirectchain of the loadinfo: + * (1) checkLoadInfoWithoutRedirects: + * checks the length of the redirectchain and tries to pop an element + * which should result in an exception and not a crash. + * (2) checkLoadInfoWithTwoRedirects: + * perform two redirects and confirm that both redirect chains + * contain the redirected URIs. + * (3) checkLoadInfoWithInternalRedirects: + * perform two redirects including CSPs upgrade-insecure-requests + * so that the redirectchain which includes internal redirects differs. + * (4) checkLoadInfoWithInternalRedirectsAndFallback + * perform two redirects including CSPs upgrade-insecure-requests + * including a 404 repsonse and hence a fallback. + * (5) checkHTTPURITruncation + * perform a redirect to a URI with an HTTP scheme to check that unwanted + * URI components are removed before being added to the redirectchain. + * (6) checkHTTPSURITruncation + * perform a redirect to a URI with an HTTPS scheme to check that unwanted + * URI components are removed before being added to the redirectchain. + */ + +SimpleTest.waitForExplicitFinish(); + +// ************** HELPERS *************** + +function compareChains(aLoadInfo, aExpectedRedirectChain, aExpectedRedirectChainIncludingInternalRedirects) { + var redirectChain = aLoadInfo.redirectChain; + var redirectChainIncludingInternalRedirects = aLoadInfo.redirectChainIncludingInternalRedirects; + + is(redirectChain.length, + aExpectedRedirectChain.length, + "confirming length of redirectChain is " + aExpectedRedirectChain.length); + + is(redirectChainIncludingInternalRedirects.length, + aExpectedRedirectChainIncludingInternalRedirects.length, + "confirming length of redirectChainIncludingInternalRedirects is " + + aExpectedRedirectChainIncludingInternalRedirects.length); +} + +function compareTruncatedChains(redirectChain, aExpectedRedirectChain) { + is(redirectChain.length, + aExpectedRedirectChain.length, + "confirming length of redirectChain is " + aExpectedRedirectChain.length); + + for (var i = 0; i < redirectChain.length; i++) { + is(redirectChain[i], + aExpectedRedirectChain[i], + "redirect chain should match expected chain"); + } +} + + +// *************** TEST 1 *************** + +function checkLoadInfoWithoutRedirects() { + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-0"); + + myXHR.onload = function() { + var loadinfo = SpecialPowers.wrap(myXHR).channel.loadInfo; + var redirectChain = loadinfo.redirectChain; + var redirectChainIncludingInternalRedirects = loadinfo.redirectChainIncludingInternalRedirects; + + is(redirectChain.length, 0, "no redirect, length should be 0"); + is(redirectChainIncludingInternalRedirects.length, 0, "no redirect, length should be 0"); + is(myXHR.responseText, "checking redirectchain", "sanity check to make sure redirects succeeded"); + + // try to pop an element from redirectChain + try { + loadinfo.popRedirectedPrincipal(false); + ok(false, "should not be possible to pop from redirectChain"); + } + catch(e) { + ok(true, "popping element from empty redirectChain should throw"); + } + + // try to pop an element from redirectChainIncludingInternalRedirects + try { + loadinfo.popRedirectedPrincipal(true); + ok(false, "should not be possible to pop from redirectChainIncludingInternalRedirects"); + } + catch(e) { + ok(true, "popping element from empty redirectChainIncludingInternalRedirects should throw"); + } + // move on to the next test + checkLoadInfoWithTwoRedirects(); + } + myXHR.onerror = function() { + ok(false, "xhr problem within checkLoadInfoWithoutRedirect()"); + } + myXHR.send(); +} + +// *************** TEST 2 *************** + +function checkLoadInfoWithTwoRedirects() { + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-2"); + + const EXPECTED_REDIRECT_CHAIN = [ + "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs", + "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs" + ]; + + const EXPECTED_REDIRECT_CHAIN_INCLUDING_INTERNAL_REDIRECTS = EXPECTED_REDIRECT_CHAIN; + + // Referrer header will not change when redirect + const EXPECTED_REFERRER = + "http://mochi.test:8888/tests/netwerk/test/mochitests/test_loadinfo_redirectchain.html"; + const isAndroid = !!navigator.userAgent.includes("Android"); + const EXPECTED_REMOTE_IP = isAndroid ? "10.0.2.2" : "127.0.0.1"; + + myXHR.onload = function() { + is(myXHR.responseText, "checking redirectchain", "sanity check to make sure redirects succeeded"); + + var loadinfo = SpecialPowers.wrap(myXHR).channel.loadInfo; + + compareChains(loadinfo, EXPECTED_REDIRECT_CHAIN, EXPECTED_REDIRECT_CHAIN_INCLUDING_INTERNAL_REDIRECTS); + + for (var i = 0; i < loadinfo.redirectChain.length; i++) { + is(loadinfo.redirectChain[i].referrerURI.spec, EXPECTED_REFERRER, "referrer should match"); + is(loadinfo.redirectChain[i].remoteAddress, EXPECTED_REMOTE_IP, "remote address should match"); + } + + // move on to the next test + checkLoadInfoWithInternalRedirects(); + } + myXHR.onerror = function() { + ok(false, "xhr problem within checkLoadInfoWithTwoRedirects()"); + } + myXHR.send(); +} + +// *************** TEST 3 *************** + +function confirmCheckLoadInfoWithInternalRedirects(event) { + const EXPECTED_REDIRECT_CHAIN = [ + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-2", + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-1" + ]; + + const EXPECTED_REDIRECT_CHAIN_INCLUDING_INTERNAL_REDIRECTS = [ + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-2", + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-2", + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-1", + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-1", + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-https-0", + ]; + + var loadinfo = JSON.parse(event.data.loadinfo); + compareChains(loadinfo, EXPECTED_REDIRECT_CHAIN, EXPECTED_REDIRECT_CHAIN_INCLUDING_INTERNAL_REDIRECTS); + + // remove the postMessage listener and move on to the next test + window.removeEventListener("message", confirmCheckLoadInfoWithInternalRedirects); + checkLoadInfoWithInternalRedirectsAndFallback(); +} + +function checkLoadInfoWithInternalRedirects() { + // load the XHR request into an iframe so we can apply a CSP to the iframe + // a postMessage returns the result back to the main page. + window.addEventListener("message", confirmCheckLoadInfoWithInternalRedirects); + document.getElementById("testframe").src = + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?iframe-redir-https-2"; +} + +// *************** TEST 4 *************** + +function confirmCheckLoadInfoWithInternalRedirectsAndFallback(event) { + var EXPECTED_REDIRECT_CHAIN = [ + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-err-2", + ]; + + var EXPECTED_REDIRECT_CHAIN_INCLUDING_INTERNAL_REDIRECTS = [ + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-err-2", + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-err-2", + "http://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-err-1", + ]; + + var loadinfo = JSON.parse(event.data.loadinfo); + compareChains(loadinfo, EXPECTED_REDIRECT_CHAIN, EXPECTED_REDIRECT_CHAIN_INCLUDING_INTERNAL_REDIRECTS); + + // remove the postMessage listener and finish test + window.removeEventListener("message", confirmCheckLoadInfoWithInternalRedirectsAndFallback); + checkHTTPURITruncation(); +} + +function checkLoadInfoWithInternalRedirectsAndFallback() { + // load the XHR request into an iframe so we can apply a CSP to the iframe + // a postMessage returns the result back to the main page. + window.addEventListener("message", confirmCheckLoadInfoWithInternalRedirectsAndFallback); + document.getElementById("testframe").src = + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?iframe-redir-err-2"; +} + +// *************** TEST 5 *************** + +function checkHTTPURITruncation() { + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "http://root:toor@mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?redir-1#baz"); + + const EXPECTED_REDIRECT_CHAIN = [ + "http://mochi.test:8888/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs", // redir-1 + ]; + + var loadinfo = SpecialPowers.wrap(myXHR).channel.loadInfo; + + myXHR.onload = function() { + var redirectChain = []; + + for (var i = 0; i < loadinfo.redirectChain.length; i++) { + redirectChain[i] = loadinfo.redirectChain[i].principal.asciiSpec; + } + + compareTruncatedChains(redirectChain, EXPECTED_REDIRECT_CHAIN); + + // move on to the next test + checkHTTPSURITruncation(); + } + myXHR.onerror = function(e) { + ok(false, "xhr problem within checkHTTPURITruncation()" + e); + } + myXHR.send(); +} + +// *************** TEST 6 *************** + +function confirmCheckHTTPSURITruncation(event) { + const EXPECTED_REDIRECT_CHAIN = [ + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs", // redir-https-2 + "https://example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs", // redir-https-1 + ]; + + var loadinfo = JSON.parse(event.data.loadinfo); + compareTruncatedChains(loadinfo.redirectChain, EXPECTED_REDIRECT_CHAIN); + + // remove the postMessage listener and move on to the next test + window.removeEventListener("message", confirmCheckHTTPSURITruncation); + SimpleTest.finish(); +} + +function checkHTTPSURITruncation() { + // load the XHR request into an iframe so we can apply a CSP to the iframe + // a postMessage returns the result back to the main page. + window.addEventListener("message", confirmCheckHTTPSURITruncation); + document.getElementById("testframe").src = + "https://root:toor@example.com/tests/netwerk/test/mochitests/file_loadinfo_redirectchain.sjs?iframe-redir-https-2#baz"; +} + +// *************** START TESTS *************** + +checkLoadInfoWithoutRedirects(); + +</script> +</body> +</html> diff --git a/netwerk/test/mochitests/test_origin_header.html b/netwerk/test/mochitests/test_origin_header.html new file mode 100644 index 0000000000..f90887ddf0 --- /dev/null +++ b/netwerk/test/mochitests/test_origin_header.html @@ -0,0 +1,398 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title> Bug 446344 - Test Origin Header</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=446344">Mozilla Bug 446344</a></p> + +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="text/javascript"> +const EMPTY_ORIGIN = "Origin: "; + +let testsToRun = [ + { + name: "sendOriginHeader=0 (never)", + prefs: [ + ["network.http.sendOriginHeader", 0], + ], + results: { + framePost: EMPTY_ORIGIN, + framePostXOrigin: EMPTY_ORIGIN, + frameGet: EMPTY_ORIGIN, + framePostNonSandboxed: EMPTY_ORIGIN, + framePostNonSandboxedXOrigin: EMPTY_ORIGIN, + framePostSandboxed: EMPTY_ORIGIN, + framePostSrcDoc: EMPTY_ORIGIN, + framePostSrcDocXOrigin: EMPTY_ORIGIN, + framePostDataURI: EMPTY_ORIGIN, + framePostSameOriginToXOrigin: EMPTY_ORIGIN, + framePostXOriginToSameOrigin: EMPTY_ORIGIN, + framePostXOriginToXOrigin: EMPTY_ORIGIN, + }, + }, + { + name: "sendOriginHeader=1 (same-origin)", + prefs: [ + ["network.http.sendOriginHeader", 1], + ], + results: { + framePost: "Origin: http://mochi.test:8888", + framePostXOrigin: "Origin: null", + frameGet: EMPTY_ORIGIN, + framePostNonSandboxed: "Origin: http://mochi.test:8888", + framePostNonSandboxedXOrigin: "Origin: null", + framePostSandboxed: "Origin: null", + framePostSrcDoc: "Origin: http://mochi.test:8888", + framePostSrcDocXOrigin: "Origin: null", + framePostDataURI: "Origin: null", + framePostSameOriginToXOrigin: "Origin: null", + framePostXOriginToSameOrigin: "Origin: null", + framePostXOriginToXOrigin: "Origin: null", + }, + }, + { + name: "sendOriginHeader=2 (always)", + prefs: [ + ["network.http.sendOriginHeader", 2], + ], + results: { + framePost: "Origin: http://mochi.test:8888", + framePostXOrigin: "Origin: http://mochi.test:8888", + frameGet: EMPTY_ORIGIN, + framePostNonSandboxed: "Origin: http://mochi.test:8888", + framePostNonSandboxedXOrigin: "Origin: http://mochi.test:8888", + framePostSandboxed: "Origin: null", + framePostSrcDoc: "Origin: http://mochi.test:8888", + framePostSrcDocXOrigin: "Origin: http://mochi.test:8888", + framePostDataURI: "Origin: null", + framePostSameOriginToXOrigin: "Origin: http://mochi.test:8888", + framePostXOriginToSameOrigin: "Origin: null", + framePostXOriginToXOrigin: "Origin: http://mochi.test:8888", + }, + }, + { + name: "sendRefererHeader=0 (never)", + prefs: [ + ["network.http.sendRefererHeader", 0], + ], + results: { + framePost: "Origin: http://mochi.test:8888", + framePostXOrigin: "Origin: http://mochi.test:8888", + frameGet: EMPTY_ORIGIN, + framePostNonSandboxed: "Origin: http://mochi.test:8888", + framePostNonSandboxedXOrigin: "Origin: http://mochi.test:8888", + framePostSandboxed: "Origin: null", + framePostSrcDoc: "Origin: http://mochi.test:8888", + framePostSrcDocXOrigin: "Origin: http://mochi.test:8888", + framePostDataURI: "Origin: null", + framePostSameOriginToXOrigin: "Origin: http://mochi.test:8888", + framePostXOriginToSameOrigin: "Origin: null", + framePostXOriginToXOrigin: "Origin: http://mochi.test:8888", + }, + }, + { + name: "userControlPolicy=0 (no-referrer)", + prefs: [ + ["network.http.sendRefererHeader", 2], + ["network.http.referer.defaultPolicy", 0], + ], + results: { + framePost: "Origin: null", + framePostXOrigin: "Origin: null", + frameGet: EMPTY_ORIGIN, + framePostNonSandboxed: "Origin: null", + framePostNonSandboxedXOrigin: "Origin: null", + framePostSandboxed: "Origin: null", + framePostSrcDoc: "Origin: null", + framePostSrcDocXOrigin: "Origin: null", + framePostDataURI: "Origin: null", + framePostSameOriginToXOrigin: "Origin: null", + framePostXOriginToSameOrigin: "Origin: null", + framePostXOriginToXOrigin: "Origin: null", + }, + }, +]; + +let checksToRun = [ + { + name: "POST", + frameID: "framePost", + formID: "formPost", + }, + { + name: "cross-origin POST", + frameID: "framePostXOrigin", + formID: "formPostXOrigin", + }, + { + name: "GET", + frameID: "frameGet", + formID: "formGet", + }, + { + name: "POST inside iframe", + frameID: "framePostNonSandboxed", + frameSrc: "HTTP://mochi.test:8888/tests/netwerk/test/mochitests/origin_header_form_post.html", + }, + { + name: "cross-origin POST inside iframe", + frameID: "framePostNonSandboxedXOrigin", + frameSrc: "Http://mochi.test:8888/tests/netwerk/test/mochitests/origin_header_form_post_xorigin.html", + }, + { + name: "POST inside sandboxed iframe", + frameID: "framePostSandboxed", + frameSrc: "http://mochi.test:8888/tests/netwerk/test/mochitests/origin_header_form_post.html", + }, + { + name: "POST inside a srcdoc iframe", + frameID: "framePostSrcDoc", + srcdoc: "origin_header_form_post.html", + }, + { + name: "cross-origin POST inside a srcdoc iframe", + frameID: "framePostSrcDocXOrigin", + srcdoc: "origin_header_form_post_xorigin.html", + }, + { + name: "POST inside a data: iframe", + frameID: "framePostDataURI", + dataURI: "origin_header_form_post.html", + }, + { + name: "same-origin POST redirected to cross-origin", + frameID: "framePostSameOriginToXOrigin", + formID: "formPostSameOriginToXOrigin", + }, + { + name: "cross-origin POST redirected to same-origin", + frameID: "framePostXOriginToSameOrigin", + formID: "formPostXOriginToSameOrigin", + }, + { + name: "cross-origin POST redirected to cross-origin", + frameID: "framePostXOriginToXOrigin", + formID: "formPostXOriginToXOrigin", + }, +]; + +function frameLoaded(test, check) +{ + let frame = window.document.getElementById(check.frameID); + frame.onload = null; + let result = SpecialPowers.wrap(frame).contentDocument.documentElement.textContent; + is(result, test.results[check.frameID], check.name + " with " + test.name); +} + +function submitForm(test, check) +{ + return new Promise((resolve, reject) => { + document.getElementById(check.frameID).onload = () => { + frameLoaded(test, check); + resolve(); + }; + document.getElementById(check.formID).submit(); + }); +} + +function loadIframe(test, check) +{ + return new Promise((resolve, reject) => { + let frame = SpecialPowers.wrap(window.document.getElementById(check.frameID)); + frame.onload = function () { + // Ignore the first load and wait for the submitted form instead. + let location = frame.contentWindow.location + ""; + if (location.endsWith("origin_header.sjs")) { + frameLoaded(test, check); + resolve(); + } + } + frame.src = check.frameSrc; + }); +} + +function loadSrcDocFrame(test, check) +{ + return new Promise((resolve, reject) => { + let frame = SpecialPowers.wrap(window.document.getElementById(check.frameID)); + frame.onload = function () { + // Ignore the first load and wait for the submitted form instead. + let location = frame.contentWindow.location + ""; + if (location.endsWith("origin_header.sjs")) { + frameLoaded(test, check); + resolve(); + } + } + fetch(check.srcdoc).then((response) => { + response.text().then((body) => { + frame.srcdoc = body; + });; + }); + }); + } + +function loadDataURIFrame(test, check) +{ + return new Promise((resolve, reject) => { + let frame = SpecialPowers.wrap(window.document.getElementById(check.frameID)); + frame.onload = function () { + // Ignore the first load and wait for the submitted form instead. + let location = frame.contentWindow.location + ""; + if (location.endsWith("origin_header.sjs")) { + frameLoaded(test, check); + resolve(); + } + } + fetch(check.dataURI).then((response) => { + response.text().then((body) => { + frame.src = "data:text/html," + encodeURIComponent(body); + });; + }); + }); +} + +async function resetFrames() +{ + let checkPromises = []; + for (let check of checksToRun) { + checkPromises.push(new Promise((resolve, reject) => { + let frame = document.getElementById(check.frameID); + frame.onload = () => resolve(); + if (check.srcdoc) { + frame.srcdoc = ""; + } else { + frame.src = "about:blank"; + } + })); + } + await Promise.all(checkPromises); +} + +async function runTests() +{ + for (let test of testsToRun) { + await resetFrames(); + await SpecialPowers.pushPrefEnv({"set": test.prefs}); + + let checkPromises = []; + for (let check of checksToRun) { + if (check.formID) { + checkPromises.push(submitForm(test, check)); + } else if (check.frameSrc) { + checkPromises.push(loadIframe(test, check)); + } else if (check.srcdoc) { + checkPromises.push(loadSrcDocFrame(test, check)); + } else if (check.dataURI) { + checkPromises.push(loadDataURIFrame(test, check)); + } else { + ok(false, "Unsupported check"); + break; + } + } + await Promise.all(checkPromises); + }; + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(5); // work around Android timeouts +addLoadEvent(runTests); + +</script> +</pre> +<table> +<tr> + <td> + <iframe src="about:blank" name="framePost" id="framePost"></iframe> + <form action="origin_header.sjs" + method="POST" + id="formPost" + target="framePost"> + <input type="submit" value="Submit POST"> + </form> + </td> + <td> + <iframe src="about:blank" name="framePostXOrigin" id="framePostXOrigin"></iframe> + <form action="http://test1.mochi.test:8888/tests/netwerk/test/mochitests/origin_header.sjs" + method="POST" + id="formPostXOrigin" + target="framePostXOrigin"> + <input type="submit" value="Submit XOrigin POST"> + </form> + </td> + <td> + <iframe src="about:blank" name="frameGet" id="frameGet"></iframe> + <form action="origin_header.sjs" + method="GET" + id="formGet" + target="frameGet"> + <input type="submit" value="Submit GET"> + </form> + </td> + <td> + <iframe src="about:blank" name="framePostSameOriginToXOrigin" id="framePostSameOriginToXOrigin"></iframe> + <form action="redirect_to.sjs?http://test1.mochi.test:8888/tests/netwerk/test/mochitests/origin_header.sjs" + method="POST" + id="formPostSameOriginToXOrigin" + target="framePostSameOriginToXOrigin"> + <input type="Submit" value="Submit SameOrigin POST redirected to XOrigin"> + </form> + </td> + <td> + <iframe src="about:blank" name="framePostXOriginToSameOrigin" id="framePostXOriginToSameOrigin"></iframe> + <form action="http://test1.mochi.test:8888/tests/netwerk/test/mochitests/redirect_to.sjs?http://mochi.test:8888/tests/netwerk/test/mochitests/origin_header.sjs" + method="POST" + id="formPostXOriginToSameOrigin" + target="framePostXOriginToSameOrigin"> + <input type="Submit" value="Submit XOrigin POST redirected to SameOrigin"> + </form> + </td> + <td> + <iframe src="about:blank" name="framePostXOriginToXOrigin" id="framePostXOriginToXOrigin"></iframe> + <form action="http://test1.mochi.test:8888/tests/netwerk/test/mochitests/redirect_to.sjs?/tests/netwerk/test/mochitests/origin_header.sjs" + method="POST" + id="formPostXOriginToXOrigin" + target="framePostXOriginToXOrigin"> + <input type="Submit" value="Submit XOrigin POST redirected to XOrigin"> + </form> + </td> +</tr> +<tr> + <td> + <iframe src="about:blank" id="framePostNonSandboxed"></iframe> + <div>Non-sandboxed iframe</div> + </td> + <td> + <iframe src="about:blank" id="framePostNonSandboxedXOrigin"></iframe> + <div>Non-sandboxed cross-origin iframe</div> + </td> + <td> + <iframe src="about:blank" id="framePostSandboxed" sandbox="allow-forms allow-scripts"></iframe> + <div>Sandboxed iframe</div> + </td> +</tr> +<tr> + <td> + <iframe id="framePostSrcDoc" src="about:blank"></iframe> + <div>Srcdoc iframe</div> + </td> + <td> + <iframe id="framePostSrcDocXOrigin" src="about:blank"></iframe> + <div>Srcdoc cross-origin iframe</div> + </td> + <td> + <iframe id="framePostDataURI" src="about:blank"></iframe> + <div>data: URI iframe</div> + </td> +</tr> +</table> + +</body> +</html> diff --git a/netwerk/test/mochitests/test_partially_cached_content.html b/netwerk/test/mochitests/test_partially_cached_content.html new file mode 100644 index 0000000000..8b6df555f8 --- /dev/null +++ b/netwerk/test/mochitests/test_partially_cached_content.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<!-- + https://bugzilla.mozilla.org/show_bug.cgi?id=497003 + + This test verifies that partially cached content is read from the cache first + and then from the network. It is written in the mochitest framework to take + thread retargeting into consideration of nsIStreamListener callbacks (inc. + nsIRequestObserver). E.g. HTML5 Stream Parser requesting retargeting of + nsIStreamListener callbacks to the parser thread. +--> +<head> + <meta charset="UTF-8"> + <title>Test for Bug 497003: support sending OnDataAvailable() to other threads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=497003">Mozilla Bug 497003: support sending OnDataAvailable() to other threads</a></p> + <p><iframe id="contentFrame" src="partial_content.sjs"></iframe></p> + +<pre id="test"> +<script> + + + +/* Check that the iframe has initial content only after the first load. + */ +function expectInitialContent(e) { + info("expectInitialContent", + "First response received: should have partial content"); + var frameElement = document.getElementById('contentFrame'); + var frameWindow = frameElement.contentWindow; + + // Expect "First response" in received HTML. + var firstResponse = frameWindow.document.getElementById('firstResponse'); + ok(firstResponse, "First response should exist"); + if (firstResponse) { + is(firstResponse.innerHTML, "First response", + "First response should be correct"); + } + + // Expect NOT to get any second response element. + var secondResponse = frameWindow.document.getElementById('secondResponse'); + ok(!secondResponse, "Should not get text for second response in first."); + + // Set up listener for second load. + removeEventListener("load", expectInitialContent, false); + frameElement.addEventListener("load", expectFullContent); + + var reload = ()=>frameElement.src = "partial_content.sjs"; + + // Before reload, disable rcwn to avoid racing and a non-range request. + SpecialPowers.pushPrefEnv({set: [["network.http.rcwn.enabled", false]]}, + reload); +} + +/* Check that the iframe has all the content after the second load. + */ +function expectFullContent(e) +{ + info("expectFullContent", + "Second response received: should complete content from first load"); + var frameWindow = document.getElementById('contentFrame').contentWindow; + + // Expect "First response" to still be there + var firstResponse = frameWindow.document.getElementById('firstResponse'); + ok(firstResponse, "First response should exist"); + if (firstResponse) { + is(firstResponse.innerHTML, "First response", + "First response should be correct"); + } + + // Expect "Second response" to be there also. + var secondResponse = frameWindow.document.getElementById('secondResponse'); + ok(secondResponse, "Second response should exist"); + if (secondResponse) { + is(secondResponse.innerHTML, "Second response", + "Second response should be correct"); + } + + SimpleTest.finish(); +} + +// Set listener for first load to expect partial content. +// Note: Set listener on the global object/window since 'load' should not fire +// for partially loaded content in an iframe. +addEventListener("load", expectInitialContent, false); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_redirect_ref.html b/netwerk/test/mochitests/test_redirect_ref.html new file mode 100644 index 0000000000..0b234695d4 --- /dev/null +++ b/netwerk/test/mochitests/test_redirect_ref.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<head> + <title> Bug 1234575 - Test redirect ref</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var iframe = document.createElement("iframe"); +iframe.src = "about:blank"; +iframe.addEventListener("load", finishTest); +document.body.appendChild(iframe); +iframe.src = "redirect.sjs#start"; + +function finishTest(e) { + is(iframe.contentWindow.location.href, "http://mochi.test:8888/tests/netwerk/test/mochitests/empty.html#"); + SimpleTest.finish(); +} + +</script> + +</body> +</html> diff --git a/netwerk/test/mochitests/test_rel_preconnect.html b/netwerk/test/mochitests/test_rel_preconnect.html new file mode 100644 index 0000000000..c7e0bada07 --- /dev/null +++ b/netwerk/test/mochitests/test_rel_preconnect.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>Test for link rel=preconnect</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +const Cc = SpecialPowers.Cc, Ci = SpecialPowers.Ci, Cr = SpecialPowers.Cr; + +var remainder = 4; +var observer; + +async function doTest() +{ + await SpecialPowers.setBoolPref("network.http.debug-observations", true); + + observer = SpecialPowers.wrapCallback(function(subject, topic, data) { + remainder--; + ok(true, "observed remainder = " + remainder); + if (!remainder) { + SpecialPowers.removeObserver(observer, "speculative-connect-request"); + SpecialPowers.setBoolPref("network.http.debug-observations", false); + SimpleTest.finish(); + } + }); + SpecialPowers.addObserver(observer, "speculative-connect-request"); + + // test the link rel=preconnect element in the head for both normal + // and crossOrigin=anonymous + var link = document.createElement("link"); + link.rel = "preconnect"; + link.href = "//localhost:8888"; + document.head.appendChild(link); + link = document.createElement("link"); + link.rel = "preconnect"; + link.href = "//localhost:8888"; + link.crossOrigin = "anonymous"; + document.head.appendChild(link); + + // test the http link response header - the test contains both a + // normal and anonymous preconnect link header + var iframe = document.createElement('iframe'); + iframe.src = 'rel_preconnect.sjs?//localhost:8888'; + + document.body.appendChild(iframe); +} + +</script> +</head> +<body onload="doTest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_same_base_domain.html b/netwerk/test/mochitests/test_same_base_domain.html new file mode 100644 index 0000000000..bcf069e8be --- /dev/null +++ b/netwerk/test/mochitests/test_same_base_domain.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Cross domain access to properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="setupTest('https://test1.example.org/tests/netwerk/test/mochitests/file_domain_inner.html', 5, 2)"> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="text/javascript" src="file_testcommon.js"> +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_same_base_domain_2.html b/netwerk/test/mochitests/test_same_base_domain_2.html new file mode 100644 index 0000000000..5647831c29 --- /dev/null +++ b/netwerk/test/mochitests/test_same_base_domain_2.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Cross domain access to properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="setupTest('https://test1.example.org/tests/netwerk/test/mochitests/file_subdomain_inner.html', 5, 2)"> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="text/javascript" src="file_testcommon.js"> +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_same_base_domain_3.html b/netwerk/test/mochitests/test_same_base_domain_3.html new file mode 100644 index 0000000000..62a4cfba95 --- /dev/null +++ b/netwerk/test/mochitests/test_same_base_domain_3.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Cross domain access to properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="setupTest('https://example.org/tests/netwerk/test/mochitests/file_subdomain_inner.html', 5, 2)"> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="text/javascript" src="file_testcommon.js"> +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_same_base_domain_4.html b/netwerk/test/mochitests/test_same_base_domain_4.html new file mode 100644 index 0000000000..87fbb1c720 --- /dev/null +++ b/netwerk/test/mochitests/test_same_base_domain_4.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Cross domain access to properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="setupTest('http://mochi.test:8888/tests/netwerk/test/mochitests/file_localhost_inner.html', 5, 2)"> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="text/javascript" src="file_testcommon.js"> +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_same_base_domain_5.html b/netwerk/test/mochitests/test_same_base_domain_5.html new file mode 100644 index 0000000000..7e4d2e3b1f --- /dev/null +++ b/netwerk/test/mochitests/test_same_base_domain_5.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Cross domain access to properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="setupTest('https://sub.sectest2.example.org/tests/netwerk/test/mochitests/file_subdomain_inner.html', 5, 2)"> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="text/javascript" src="file_testcommon.js"> +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_same_base_domain_6.html b/netwerk/test/mochitests/test_same_base_domain_6.html new file mode 100644 index 0000000000..195c38657b --- /dev/null +++ b/netwerk/test/mochitests/test_same_base_domain_6.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Cross domain access to properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="runThisTest()"> +<p id="display"></p> +<pre id="test"> + <script> + function runThisTest() { + // By default, proxies don't apply to 127.0.0.1. + // We need them to for this test (at least on android), though: + SpecialPowers.pushPrefEnv({set: [ + ["network.proxy.allow_hijacking_localhost", true] + ]}).then(function() { + setupTest('http://127.0.0.1:8888/tests/netwerk/test/mochitests/file_loopback_inner.html', 5, 2); + }); + } + </script> +<script class="testbody" type="text/javascript" src="file_testcommon.js"> +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_samedomain.html b/netwerk/test/mochitests/test_samedomain.html new file mode 100644 index 0000000000..82aace8bab --- /dev/null +++ b/netwerk/test/mochitests/test_samedomain.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Cross domain access to properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="setupTest('http://example.org/tests/netwerk/test/mochitests/file_domain_inner.html', 5, 2)"> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="text/javascript" src="file_testcommon.js"> +</script> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_uri_scheme.html b/netwerk/test/mochitests/test_uri_scheme.html new file mode 100644 index 0000000000..b0c247f336 --- /dev/null +++ b/netwerk/test/mochitests/test_uri_scheme.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>Test for URI Manipulation</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> +function dotest1() +{ + SimpleTest.waitForExplicitFinish(); + var o = new URL("http://localhost/"); + try { o.href = "foopy:bar:baz"; } catch(e) { } + o.protocol = "http:"; + o.hostname; + try { o.href = "http://localhost/"; } catch(e) { } + ok(o.protocol, "http:"); + dotest2(); +} + +function dotest2() +{ + var o = new URL("http://www.mozilla.org/"); + try { + o.href ="aaaaaaaaaaa:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; + } catch(e) { } + o.hash = "#"; + o.pathname = "/"; + o.protocol = "http:"; + try { o.href = "http://localhost/"; } catch(e) { } + ok(o.protocol, "http:"); + dotest3(); +} + +function dotest3() +{ + is(new URL("resource://123/").href, "resource://123/"); + SimpleTest.finish(); +} +</script> +</head> +<body onload="dotest1();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_viewsource_unlinkable.html b/netwerk/test/mochitests/test_viewsource_unlinkable.html new file mode 100644 index 0000000000..f4c4064183 --- /dev/null +++ b/netwerk/test/mochitests/test_viewsource_unlinkable.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<!-- +--> +<head> + <title>Test for view-source linkability</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> +function runTest() { + SimpleTest.doesThrow(function() { + window.open('view-source:' + location.href, "_blank"); + }, "Trying to access view-source URL from unprivileged code should throw."); + SimpleTest.finish(); +} +</script> +</head> +<body onload="runTest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/test_xhr_method_case.html b/netwerk/test/mochitests/test_xhr_method_case.html new file mode 100644 index 0000000000..ddb830328c --- /dev/null +++ b/netwerk/test/mochitests/test_xhr_method_case.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +XHR uppercases certain method names, but not others +--> +<head> + <title>Test for XHR Method casing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> + +const testMethods = [ +// these methods should be normalized + ["get", "GET"], + ["GET", "GET"], + ["GeT", "GET"], + ["geT", "GET"], + ["GEt", "GET"], + ["post", "POST"], + ["POST", "POST"], + ["delete", "DELETE"], + ["DELETE", "DELETE"], + ["options", "OPTIONS"], + ["OPTIONS", "OPTIONS"], + ["put", "PUT"], + ["PUT", "PUT"], +// HEAD is not tested because we use the resposne body as part of the test +// ["head", "HEAD"], +// ["HEAD", "HEAD"], + +// other custom methods should not be normalized + ["Foo", "Foo"], + ["bAR", "bAR"], + ["foobar", "foobar"], + ["FOOBAR", "FOOBAR"] +] + +function doIter(index) +{ + var xhr = new XMLHttpRequest(); + xhr.open(testMethods[index][0], 'method.sjs', false); // sync request + xhr.send(); + is(xhr.status, 200, 'transaction failed'); + is(xhr.response, testMethods[index][1], 'unexpected method'); +} + +function dotest() +{ + SimpleTest.waitForExplicitFinish(); + for (var i = 0; i < testMethods.length; i++) { + doIter(i); + } + SimpleTest.finish(); +} + +</script> +</head> +<body onload="dotest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/netwerk/test/mochitests/web_packaged_app.sjs b/netwerk/test/mochitests/web_packaged_app.sjs new file mode 100644 index 0000000000..772b8a0835 --- /dev/null +++ b/netwerk/test/mochitests/web_packaged_app.sjs @@ -0,0 +1,47 @@ +function handleRequest(request, response) { + response.setHeader("Content-Type", "application/package", false); + response.write(octetStreamData.getData()); +} + +// The package content +// getData formats it as described at http://www.w3.org/TR/web-packaging/#streamable-package-format +var octetStreamData = { + content: [ + { + headers: ["Content-Location: /index.html", "Content-Type: text/html"], + data: "<html>\r\n <head>\r\n <script> alert('OK: hello'); alert('DONE'); </script>\r\n</head>\r\n Web Packaged App Index\r\n</html>\r\n", + type: "text/html", + }, + { + headers: [ + "Content-Location: /scripts/app.js", + "Content-Type: text/javascript", + ], + data: "module Math from '/scripts/helpers/math.js';\r\n...\r\n", + type: "text/javascript", + }, + { + headers: [ + "Content-Location: /scripts/helpers/math.js", + "Content-Type: text/javascript", + ], + data: "export function sum(nums) { ... }\r\n...\r\n", + type: "text/javascript", + }, + ], + token: "gc0pJq0M:08jU534c0p", + getData() { + var str = ""; + for (var i in this.content) { + str += "--" + this.token + "\r\n"; + for (var j in this.content[i].headers) { + str += this.content[i].headers[j] + "\r\n"; + } + str += "\r\n"; + str += this.content[i].data + "\r\n"; + } + + str += "--" + this.token + "--"; + return str; + }, +}; diff --git a/netwerk/test/moz.build b/netwerk/test/moz.build new file mode 100644 index 0000000000..07895c8f43 --- /dev/null +++ b/netwerk/test/moz.build @@ -0,0 +1,34 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +TEST_DIRS += ["httpserver", "gtest", "http3server"] + +BROWSER_CHROME_MANIFESTS += [ + "browser/browser.ini", + "useragent/browser_nonsnap.ini", + "useragent/browser_snap.ini", +] +MOCHITEST_MANIFESTS += ["mochitests/mochitest.ini"] + +XPCSHELL_TESTS_MANIFESTS += [ + "unit/xpcshell.ini", + "unit_ipc/xpcshell.ini", +] + +TESTING_JS_MODULES += [ + "browser/cookie_filtering_helper.sys.mjs", + "browser/early_hint_preload_test_helper.sys.mjs", + "unit/test_http3_prio_helpers.js", +] + +PERFTESTS_MANIFESTS += ["perf/perftest.ini", "unit/perftest.ini"] + +MARIONETTE_UNIT_MANIFESTS += [ + "marionette/manifest.ini", +] + +if CONFIG["FUZZING_INTERFACES"]: + TEST_DIRS += ["fuzz"] diff --git a/netwerk/test/perf/.eslintrc.js b/netwerk/test/perf/.eslintrc.js new file mode 100644 index 0000000000..f040032509 --- /dev/null +++ b/netwerk/test/perf/.eslintrc.js @@ -0,0 +1,12 @@ +/* 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"; + +module.exports = { + env: { + browser: true, + node: true, + }, +}; diff --git a/netwerk/test/perf/hooks_throttling.py b/netwerk/test/perf/hooks_throttling.py new file mode 100644 index 0000000000..5f46b3f0d3 --- /dev/null +++ b/netwerk/test/perf/hooks_throttling.py @@ -0,0 +1,202 @@ +# 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/. +""" +Drives the throttling feature when the test calls our +controlled server. +""" +import http.client +import json +import os +import sys +import time +from urllib.parse import urlparse + +from mozperftest.test.browsertime import add_option +from mozperftest.utils import get_tc_secret + +ENDPOINTS = { + "linux": "h3.dev.mozaws.net", + "darwin": "h3.mac.dev.mozaws.net", + "win32": "h3.win.dev.mozaws.net", +} +CTRL_SERVER = ENDPOINTS[sys.platform] +TASK_CLUSTER = "TASK_ID" in os.environ.keys() +_SECRET = { + "throttler_host": f"https://{CTRL_SERVER}/_throttler", + "throttler_key": os.environ.get("WEBNETEM_KEY", ""), +} +if TASK_CLUSTER: + _SECRET.update(get_tc_secret()) + +if _SECRET["throttler_key"] == "": + if TASK_CLUSTER: + raise Exception("throttler_key not found in secret") + raise Exception("WEBNETEM_KEY not set") + +_TIMEOUT = 30 +WAIT_TIME = 60 * 10 +IDLE_TIME = 10 +BREATHE_TIME = 20 + + +class Throttler: + def __init__(self, env, host, key): + self.env = env + self.host = host + self.key = key + self.verbose = env.get_arg("verbose", False) + self.logger = self.verbose and self.env.info or self.env.debug + + def log(self, msg): + self.logger("[throttler] " + msg) + + def _request(self, action, data=None): + kw = {} + headers = {b"X-WEBNETEM-KEY": self.key} + verb = data is None and "GET" or "POST" + if data is not None: + data = json.dumps(data) + headers[b"Content-type"] = b"application/json" + + parsed = urlparse(self.host) + server = parsed.netloc + path = parsed.path + if action != "status": + path += "/" + action + + self.log(f"Calling {verb} {path}") + conn = http.client.HTTPSConnection(server, timeout=_TIMEOUT) + conn.request(verb, path, body=data, headers=headers, **kw) + resp = conn.getresponse() + res = resp.read() + if resp.status >= 400: + raise Exception(res) + res = json.loads(res) + return res + + def start(self, data=None): + self.log("Starting") + now = time.time() + acquired = False + + while time.time() - now < WAIT_TIME: + status = self._request("status") + if status.get("test_running"): + # a test is running + self.log("A test is already controlling the server") + self.log(f"Waiting {IDLE_TIME} seconds") + else: + try: + self._request("start_test") + acquired = True + break + except Exception: + # we got beat in the race + self.log("Someone else beat us") + time.sleep(IDLE_TIME) + + if not acquired: + raise Exception("Could not acquire the test server") + + if data is not None: + self._request("shape", data) + + def stop(self): + self.log("Stopping") + try: + self._request("reset") + finally: + self._request("stop_test") + + +def get_throttler(env): + host = _SECRET["throttler_host"] + key = _SECRET["throttler_key"].encode() + return Throttler(env, host, key) + + +_PROTOCOL = "h2", "h3" +_PAGE = "gallery", "news", "shopping", "photoblog" + +# set the network condition here. +# each item has a name and some netem options: +# +# loss_ratio: specify percentage of packets that will be lost +# loss_corr: specify a correlation factor for the random packet loss +# dup_ratio: specify percentage of packets that will be duplicated +# delay: specify an overall delay for each packet +# jitter: specify amount of jitter in milliseconds +# delay_jitter_corr: specify a correlation factor for the random jitter +# reorder_ratio: specify percentage of packets that will be reordered +# reorder_corr: specify a correlation factor for the random reordering +# +_THROTTLING = ( + {"name": "full"}, # no throttling. + {"name": "one", "delay": "20"}, + {"name": "two", "delay": "50"}, + {"name": "three", "delay": "100"}, + {"name": "four", "delay": "200"}, + {"name": "five", "delay": "300"}, +) + + +def get_test(): + """Iterate on test conditions. + + For each cycle, we return a combination of: protocol, page, throttling + settings. Each combination has a name, and that name will be used along with + the protocol as a prefix for each metrics. + """ + for proto in _PROTOCOL: + for page in _PAGE: + url = f"https://{CTRL_SERVER}/{page}.html" + for throttler_settings in _THROTTLING: + yield proto, page, url, throttler_settings + + +combo = get_test() + + +def before_cycle(metadata, env, cycle, script): + global combo + if "throttlable" not in script["tags"]: + return + throttler = get_throttler(env) + try: + proto, page, url, throttler_settings = next(combo) + except StopIteration: + combo = get_test() + proto, page, url, throttler_settings = next(combo) + + # setting the url for the browsertime script + add_option(env, "browsertime.url", url, overwrite=True) + + # enabling http if needed + if proto == "h3": + add_option(env, "firefox.preference", "network.http.http3.enable:true") + + # prefix used to differenciate metrics + name = throttler_settings["name"] + script["name"] = f"{name}_{proto}_{page}" + + # throttling the controlled server if needed + if throttler_settings != {"name": "full"}: + env.info("Calling the controlled server") + throttler.start(throttler_settings) + else: + env.info("No throttling for this call") + throttler.start() + + +def after_cycle(metadata, env, cycle, script): + if "throttlable" not in script["tags"]: + return + throttler = get_throttler(env) + try: + throttler.stop() + except Exception: + pass + + # give a chance for a competitive job to take over + time.sleep(BREATHE_TIME) diff --git a/netwerk/test/perf/perftest.ini b/netwerk/test/perf/perftest.ini new file mode 100644 index 0000000000..aa96f97bf6 --- /dev/null +++ b/netwerk/test/perf/perftest.ini @@ -0,0 +1,8 @@ +[perftest_http3_cloudflareblog.js] +[perftest_http3_controlled.js] +[perftest_http3_facebook_scroll.js] +[perftest_http3_google_image.js] +[perftest_http3_google_search.js] +[perftest_http3_lucasquicfetch.js] +[perftest_http3_youtube_watch.js] +[perftest_http3_youtube_watch_scroll.js] diff --git a/netwerk/test/perf/perftest_http3_cloudflareblog.js b/netwerk/test/perf/perftest_http3_cloudflareblog.js new file mode 100644 index 0000000000..4bbea56bbd --- /dev/null +++ b/netwerk/test/perf/perftest_http3_cloudflareblog.js @@ -0,0 +1,56 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +/* eslint-env node */ + +/* +Ensure the `--firefox.preference=network.http.http3.enable:true` is +set for this test. +*/ + +async function test(context, commands) { + let rootUrl = "https://blog.cloudflare.com/"; + let waitTime = 1000; + + if ( + (typeof context.options.browsertime !== "undefined") & + (typeof context.options.browsertime.waitTime !== "undefined") + ) { + waitTime = context.options.browsertime.waitTime; + } + + // Make firefox learn of HTTP/3 server + // XXX: Need to build an HTTP/3-specific conditioned profile + // to handle these pre-navigations. + await commands.navigate(rootUrl); + + let cycles = 1; + for (let cycle = 0; cycle < cycles; cycle++) { + // Measure initial pageload + await commands.measure.start("pageload"); + await commands.navigate(rootUrl); + await commands.measure.stop(); + commands.measure.result[0].browserScripts.pageinfo.url = + "Cloudflare Blog - Main"; + + // Wait for X seconds + await commands.wait.byTime(waitTime); + + // Measure navigation pageload + await commands.measure.start("pageload"); + await commands.click.byJsAndWait(` + document.querySelectorAll("article")[0].querySelector("a") + `); + await commands.measure.stop(); + commands.measure.result[1].browserScripts.pageinfo.url = + "Cloudflare Blog - Article"; + } +} + +module.exports = { + test, + owner: "Network Team", + name: "cloudflare", + component: "netwerk", + description: "User-journey live site test for cloudflare blog.", +}; diff --git a/netwerk/test/perf/perftest_http3_controlled.js b/netwerk/test/perf/perftest_http3_controlled.js new file mode 100644 index 0000000000..243c314b5b --- /dev/null +++ b/netwerk/test/perf/perftest_http3_controlled.js @@ -0,0 +1,32 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +/* eslint-env node */ + +/* +Ensure the `--firefox.preference=network.http.http3.enable:true` is +set for this test. +*/ + +async function test(context, commands) { + let url = context.options.browsertime.url; + + // Make firefox learn of HTTP/3 server + // XXX: Need to build an HTTP/3-specific conditioned profile + // to handle these pre-navigations. + await commands.navigate(url); + + // Measure initial pageload + await commands.measure.start("pageload"); + await commands.navigate(url); + await commands.measure.stop(); + commands.measure.result[0].browserScripts.pageinfo.url = url; +} + +module.exports = { + test, + owner: "Network Team", + name: "controlled", + description: "User-journey live site test for controlled server", + tags: ["throttlable"], +}; diff --git a/netwerk/test/perf/perftest_http3_facebook_scroll.js b/netwerk/test/perf/perftest_http3_facebook_scroll.js new file mode 100644 index 0000000000..7e736a0f5f --- /dev/null +++ b/netwerk/test/perf/perftest_http3_facebook_scroll.js @@ -0,0 +1,165 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +/* eslint-env node */ + +/* +Ensure the `--firefox.preference=network.http.http3.enable:true` is +set for this test. +*/ + +async function captureNetworkRequest(commands) { + var capture_network_request = []; + var capture_resource = await commands.js.run(` + return performance.getEntriesByType("resource"); + `); + for (var i = 0; i < capture_resource.length; i++) { + capture_network_request.push(capture_resource[i].name); + } + return capture_network_request; +} + +async function waitForScrollRequestsEnd( + prevCount, + maxStableCount, + timeout, + commands, + context +) { + let starttime = await commands.js.run(`return performance.now();`); + let endtime = await commands.js.run(`return performance.now();`); + let changing = true; + let newCount = -1; + let stableCount = 0; + + while ( + ((await commands.js.run(`return performance.now();`)) - starttime < + timeout) & + changing + ) { + // Wait a bit before making another round + await commands.wait.byTime(100); + newCount = (await captureNetworkRequest(commands)).length; + context.log.debug(`${newCount}, ${prevCount}, ${stableCount}`); + + // Check if we are approaching stability + if (newCount == prevCount) { + // Gather the end time now + if (stableCount == 0) { + endtime = await commands.js.run(`return performance.now();`); + } + stableCount++; + } else { + prevCount = newCount; + stableCount = 0; + } + + if (stableCount >= maxStableCount) { + // Stability achieved + changing = false; + } + } + + return { + start: starttime, + end: endtime, + numResources: newCount, + }; +} + +async function test(context, commands) { + let rootUrl = "https://www.facebook.com/lambofgod/"; + let waitTime = 1000; + let numScrolls = 5; + + const average = arr => arr.reduce((p, c) => p + c, 0) / arr.length; + + if (typeof context.options.browsertime !== "undefined") { + if (typeof context.options.browsertime.waitTime !== "undefined") { + waitTime = context.options.browsertime.waitTime; + } + if (typeof context.options.browsertime.numScrolls !== "undefined") { + numScrolls = context.options.browsertime.numScrolls; + } + } + + // Make firefox learn of HTTP/3 server + await commands.navigate(rootUrl); + + let cycles = 1; + for (let cycle = 0; cycle < cycles; cycle++) { + // Measure the pageload + await commands.measure.start("pageload"); + await commands.navigate(rootUrl); + await commands.measure.stop(); + + // Initial scroll to make the new user popup show + await commands.js.runAndWait( + `window.scrollTo({ top: 1000, behavior: 'smooth' })` + ); + await commands.wait.byTime(1000); + await commands.click.byLinkTextAndWait("Not Now"); + + let vals = []; + let badIterations = 0; + for (let iteration = 0; iteration < numScrolls; iteration++) { + // Clear old resources + await commands.js.run(`performance.clearResourceTimings();`); + + // Get current resource count + let currCount = (await captureNetworkRequest(commands)).length; + + // Scroll to a ridiculously high value for "infinite" down-scrolling + commands.js.runAndWait(` + window.scrollTo({ top: 100000000 }) + `); + + /* + The maxStableCount of 22 was chosen as a trade-off between fast iterations + and minimizing the number of bad iterations. + */ + let newInfo = await waitForScrollRequestsEnd( + currCount, + 22, + 120000, + commands, + context + ); + + // Gather metrics + let ndiff = newInfo.numResources - currCount; + let tdiff = (newInfo.end - newInfo.start) / 1000; + + // Check if we had a bad iteration + if (ndiff == 0) { + context.log.info("Bad iteration, redoing..."); + iteration--; + badIterations++; + if (badIterations == 5) { + throw new Error("Too many bad scroll iterations occurred"); + } + continue; + } + + vals.push(ndiff / tdiff); + + // Wait X seconds before scrolling again + await commands.wait.byTime(waitTime); + } + + if (!vals.length) { + throw new Error("No requestsPerSecond values were obtained"); + } + + commands.measure.result[0].browserScripts.pageinfo.requestsPerSecond = + average(vals); + } +} + +module.exports = { + test, + owner: "Network Team", + component: "netwerk", + name: "facebook-scroll", + description: "Measures the number of requests per second after a scroll.", +}; diff --git a/netwerk/test/perf/perftest_http3_google_image.js b/netwerk/test/perf/perftest_http3_google_image.js new file mode 100644 index 0000000000..8d0f788245 --- /dev/null +++ b/netwerk/test/perf/perftest_http3_google_image.js @@ -0,0 +1,190 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +/* eslint-env node */ + +/* +Ensure the `--firefox.preference=network.http.http3.enable:true` is +set for this test. +*/ + +async function getNumImagesLoaded(elementSelector, commands) { + return commands.js.run(` + let sum = 0; + document.querySelectorAll(${elementSelector}).forEach(e => { + sum += e.complete & e.naturalHeight != 0; + }); + return sum; + `); +} + +async function waitForImgLoadEnd( + prevCount, + maxStableCount, + iterationDelay, + timeout, + commands, + context, + counter, + elementSelector +) { + let starttime = await commands.js.run(`return performance.now();`); + let endtime = await commands.js.run(`return performance.now();`); + let changing = true; + let newCount = -1; + let stableCount = 0; + + while ( + ((await commands.js.run(`return performance.now();`)) - starttime < + timeout) & + changing + ) { + // Wait a bit before making another round + await commands.wait.byTime(iterationDelay); + newCount = await counter(elementSelector, commands); + context.log.debug(`${newCount}, ${prevCount}, ${stableCount}`); + + // Check if we are approaching stability + if (newCount == prevCount) { + // Gather the end time now + if (stableCount == 0) { + endtime = await commands.js.run(`return performance.now();`); + } + stableCount++; + } else { + prevCount = newCount; + stableCount = 0; + } + + if (stableCount >= maxStableCount) { + // Stability achieved + changing = false; + } + } + + return { + start: starttime, + end: endtime, + numResources: newCount, + }; +} + +async function test(context, commands) { + let rootUrl = "https://www.google.com/search?q=kittens&tbm=isch"; + let waitTime = 1000; + let numScrolls = 10; + + const average = arr => arr.reduce((p, c) => p + c, 0) / arr.length; + + if (typeof context.options.browsertime !== "undefined") { + if (typeof context.options.browsertime.waitTime !== "undefined") { + waitTime = context.options.browsertime.waitTime; + } + if (typeof context.options.browsertime.numScrolls !== "undefined") { + numScrolls = context.options.browsertime.numScrolls; + } + } + + // Make firefox learn of HTTP/3 server + await commands.navigate(rootUrl); + + let cycles = 1; + for (let cycle = 0; cycle < cycles; cycle++) { + // Measure the pageload + await commands.measure.start("pageload"); + await commands.navigate(rootUrl); + await commands.measure.stop(); + + async function getHeight() { + return commands.js.run(`return document.body.scrollHeight;`); + } + + let vals = []; + let badIterations = 0; + let prevHeight = 0; + for (let iteration = 0; iteration < numScrolls; iteration++) { + // Get current image count + let currCount = await getNumImagesLoaded(`".isv-r img"`, commands); + prevHeight = await getHeight(); + + // Scroll to a ridiculously high value for "infinite" down-scrolling + commands.js.runAndWait(` + window.scrollTo({ top: 100000000 }) + `); + + /* + The maxStableCount of 22 was chosen as a trade-off between fast iterations + and minimizing the number of bad iterations. + */ + let results = await waitForImgLoadEnd( + currCount, + 22, + 100, + 120000, + commands, + context, + getNumImagesLoaded, + `".isv-r img"` + ); + + // Gather metrics + let ndiff = results.numResources - currCount; + let tdiff = (results.end - results.start) / 1000; + + // Check if we had a bad iteration + if (ndiff == 0) { + // Check if the end of the search results was reached + if (prevHeight == (await getHeight())) { + context.log.info("Reached end of page."); + break; + } + context.log.info("Bad iteration, redoing..."); + iteration--; + badIterations++; + if (badIterations == 5) { + throw new Error("Too many bad scroll iterations occurred"); + } + continue; + } + + context.log.info(`${ndiff}, ${tdiff}`); + vals.push(ndiff / tdiff); + + // Wait X seconds before scrolling again + await commands.wait.byTime(waitTime); + } + + if (!vals.length) { + throw new Error("No requestsPerSecond values were obtained"); + } + + commands.measure.result[0].browserScripts.pageinfo.imagesPerSecond = + average(vals); + + // Test clicking and and opening an image + await commands.wait.byTime(waitTime); + commands.click.byJs(` + const links = document.querySelectorAll(".islib"); links[links.length-1] + `); + let results = await waitForImgLoadEnd( + 0, + 22, + 50, + 120000, + commands, + context, + getNumImagesLoaded, + `"#islsp img"` + ); + commands.measure.result[0].browserScripts.pageinfo.imageLoadTime = + results.end - results.start; + } +} + +module.exports = { + test, + owner: "Network Team", + component: "netwerk", + name: "g-image", + description: "Measures the number of images per second after a scroll.", +}; diff --git a/netwerk/test/perf/perftest_http3_google_search.js b/netwerk/test/perf/perftest_http3_google_search.js new file mode 100644 index 0000000000..8183e3a152 --- /dev/null +++ b/netwerk/test/perf/perftest_http3_google_search.js @@ -0,0 +1,73 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +/* eslint-env node */ + +/* +Ensure the `--firefox.preference=network.http.http3.enable:true` is +set for this test. +*/ + +async function test(context, commands) { + let rootUrl = "https://www.google.com/"; + let waitTime = 1000; + const driver = context.selenium.driver; + const webdriver = context.selenium.webdriver; + + if ( + (typeof context.options.browsertime !== "undefined") & + (typeof context.options.browsertime.waitTime !== "undefined") + ) { + waitTime = context.options.browsertime.waitTime; + } + + // Make firefox learn of HTTP/3 server + await commands.navigate(rootUrl); + + let cycles = 1; + for (let cycle = 0; cycle < cycles; cycle++) { + await commands.navigate(rootUrl); + await commands.wait.byTime(1000); + + // Set up the search + context.log.info("Setting up search"); + const searchfield = driver.findElement(webdriver.By.name("q")); + searchfield.sendKeys("Python\n"); + await commands.wait.byTime(5000); + + // Measure the search time + context.log.info("Start search"); + await commands.measure.start("pageload"); + await commands.click.byJs(`document.querySelector("input[name='btnK']")`); + await commands.wait.byTime(5000); + await commands.measure.stop(); + context.log.info("Done"); + + commands.measure.result[0].browserScripts.pageinfo.url = + "Google Search (Python)"; + + // Wait for X seconds + context.log.info(`Waiting for ${waitTime} milliseconds`); + await commands.wait.byTime(waitTime); + + // Go to the next search page and measure + context.log.info("Going to second page of search results"); + await commands.measure.start("pageload"); + await commands.click.byIdAndWait("pnnext"); + + // XXX: Increase wait time when we add latencies + await commands.wait.byTime(3000); + await commands.measure.stop(); + + commands.measure.result[1].browserScripts.pageinfo.url = + "Google Search (Python) - Next Page"; + } +} + +module.exports = { + test, + owner: "Network Team", + component: "netwerk", + name: "g-search", + description: "User-journey live site test for google search", +}; diff --git a/netwerk/test/perf/perftest_http3_lucasquicfetch.js b/netwerk/test/perf/perftest_http3_lucasquicfetch.js new file mode 100644 index 0000000000..49a5b4c824 --- /dev/null +++ b/netwerk/test/perf/perftest_http3_lucasquicfetch.js @@ -0,0 +1,134 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +/* eslint-env node */ + +/* +Ensure the `--firefox.preference=network.http.http3.enable:true` is +set for this test. +*/ + +async function getNumLoaded(commands) { + return commands.js.run(` + let sum = 0; + document.querySelectorAll("#imgContainer img").forEach(e => { + sum += e.complete & e.naturalHeight != 0; + }); + return sum; + `); +} + +async function waitForImgLoadEnd( + prevCount, + maxStableCount, + timeout, + commands, + context +) { + let starttime = await commands.js.run(`return performance.now();`); + let endtime = await commands.js.run(`return performance.now();`); + let changing = true; + let newCount = -1; + let stableCount = 0; + + while ( + ((await commands.js.run(`return performance.now();`)) - starttime < + timeout) & + changing + ) { + // Wait a bit before making another round + await commands.wait.byTime(100); + newCount = await getNumLoaded(commands); + context.log.debug(`${newCount}, ${prevCount}, ${stableCount}`); + + // Check if we are approaching stability + if (newCount == prevCount) { + // Gather the end time now + if (stableCount == 0) { + endtime = await commands.js.run(`return performance.now();`); + } + stableCount++; + } else { + prevCount = newCount; + stableCount = 0; + } + + if (stableCount >= maxStableCount) { + // Stability achieved + changing = false; + } + } + + return { + start: starttime, + end: endtime, + numResources: newCount, + }; +} + +async function test(context, commands) { + let rootUrl = "https://lucaspardue.com/quictilesfetch.html"; + let cycles = 5; + + if ( + (typeof context.options.browsertime !== "undefined") & + (typeof context.options.browsertime.cycles !== "undefined") + ) { + cycles = context.options.browsertime.cycles; + } + + // Make firefox learn of HTTP/3 server + // XXX: Need to build an HTTP/3-specific conditioned profile + // to handle these pre-navigations. + await commands.navigate(rootUrl); + + let combos = [ + [100, 1], + [100, 100], + [300, 300], + ]; + for (let cycle = 0; cycle < cycles; cycle++) { + for (let combo = 0; combo < combos.length; combo++) { + await commands.measure.start("pageload"); + await commands.navigate(rootUrl); + await commands.measure.stop(); + let last = commands.measure.result.length - 1; + commands.measure.result[ + last + ].browserScripts.pageinfo.url = `LucasQUIC (r=${combos[combo][0]}, p=${combos[combo][1]})`; + + // Set the input fields + await commands.js.runAndWait(` + document.querySelector("#maxReq").setAttribute( + "value", + ${combos[combo][0]} + ) + `); + await commands.js.runAndWait(` + document.querySelector("#reqGroup").setAttribute( + "value", + ${combos[combo][1]} + ) + `); + + // Start the test and wait for the images to finish loading + commands.click.byJs(`document.querySelector("button")`); + let results = await waitForImgLoadEnd(0, 40, 120000, commands, context); + + commands.measure.result[last].browserScripts.pageinfo.resourceLoadTime = + results.end - results.start; + commands.measure.result[last].browserScripts.pageinfo.imagesLoaded = + results.numResources; + commands.measure.result[last].browserScripts.pageinfo.imagesMissed = + combos[combo][0] - results.numResources; + } + } +} + +module.exports = { + test, + owner: "Network Team", + name: "lq-fetch", + component: "netwerk", + description: "Measures the amount of time it takes to load a set of images.", +}; diff --git a/netwerk/test/perf/perftest_http3_youtube_watch.js b/netwerk/test/perf/perftest_http3_youtube_watch.js new file mode 100644 index 0000000000..1fd525941d --- /dev/null +++ b/netwerk/test/perf/perftest_http3_youtube_watch.js @@ -0,0 +1,74 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +/* eslint-env node */ + +/* +Ensure the `--firefox.preference=network.http.http3.enable:true` is +set for this test. +*/ + +async function test(context, commands) { + let rootUrl = "https://www.youtube.com/watch?v=COU5T-Wafa4"; + let waitTime = 20000; + + if ( + (typeof context.options.browsertime !== "undefined") & + (typeof context.options.browsertime.waitTime !== "undefined") + ) { + waitTime = context.options.browsertime.waitTime; + } + + // Make firefox learn of HTTP/3 server + await commands.navigate(rootUrl); + + let cycles = 1; + for (let cycle = 0; cycle < cycles; cycle++) { + await commands.measure.start("pageload"); + await commands.navigate(rootUrl); + + // Make sure the video is running + if ( + await commands.js.run(`return document.querySelector("video").paused;`) + ) { + throw new Error("Video should be running but it's paused"); + } + + // Disable youtube autoplay + await commands.click.byIdAndWait("toggleButton"); + + // Start playback quality measurements + const start = await commands.js.run(`return performance.now();`); + while ( + !(await commands.js.run(` + return document.querySelector("video").ended; + `)) & + !(await commands.js.run(` + return document.querySelector("video").paused; + `)) & + ((await commands.js.run(`return performance.now();`)) - start < waitTime) + ) { + await commands.wait.byTime(5000); + context.log.info("playing..."); + } + + // Video done, now gather metrics + const playbackQuality = await commands.js.run( + `return document.querySelector("video").getVideoPlaybackQuality();` + ); + await commands.measure.stop(); + + commands.measure.result[0].browserScripts.pageinfo.droppedFrames = + playbackQuality.droppedVideoFrames; + commands.measure.result[0].browserScripts.pageinfo.decodedFrames = + playbackQuality.totalVideoFrames; + } +} + +module.exports = { + test, + owner: "Network Team", + component: "netwerk", + name: "youtube-noscroll", + description: "Measures quality of the video being played.", +}; diff --git a/netwerk/test/perf/perftest_http3_youtube_watch_scroll.js b/netwerk/test/perf/perftest_http3_youtube_watch_scroll.js new file mode 100644 index 0000000000..8f30fcc5e6 --- /dev/null +++ b/netwerk/test/perf/perftest_http3_youtube_watch_scroll.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/. +/* eslint-env node */ + +/* +Ensure the `--firefox.preference=network.http.http3.enable:true` is +set for this test. +*/ + +async function test(context, commands) { + let rootUrl = "https://www.youtube.com/watch?v=COU5T-Wafa4"; + let waitTime = 20000; + + if ( + (typeof context.options.browsertime !== "undefined") & + (typeof context.options.browsertime.waitTime !== "undefined") + ) { + waitTime = context.options.browsertime.waitTime; + } + + // Make firefox learn of HTTP/3 server + await commands.navigate(rootUrl); + + let cycles = 1; + for (let cycle = 0; cycle < cycles; cycle++) { + await commands.measure.start("pageload"); + await commands.navigate(rootUrl); + + // Make sure the video is running + // XXX: Should we start the video ourself? + if ( + await commands.js.run(`return document.querySelector("video").paused;`) + ) { + throw new Error("Video should be running but it's paused"); + } + + // Disable youtube autoplay + await commands.click.byIdAndWait("toggleButton"); + + // Start playback quality measurements + const start = await commands.js.run(`return performance.now();`); + let counter = 1; + let direction = 0; + while ( + !(await commands.js.run(` + return document.querySelector("video").ended; + `)) & + !(await commands.js.run(` + return document.querySelector("video").paused; + `)) & + ((await commands.js.run(`return performance.now();`)) - start < waitTime) + ) { + // Reset the scroll after going down 10 times + direction = counter * 1000; + if (direction > 10000) { + counter = -1; + } + counter++; + + await commands.js.runAndWait( + `window.scrollTo({ top: ${direction}, behavior: 'smooth' })` + ); + context.log.info("playing while scrolling..."); + } + + // Video done, now gather metrics + const playbackQuality = await commands.js.run( + `return document.querySelector("video").getVideoPlaybackQuality();` + ); + await commands.measure.stop(); + + commands.measure.result[0].browserScripts.pageinfo.droppedFrames = + playbackQuality.droppedVideoFrames; + commands.measure.result[0].browserScripts.pageinfo.decodedFrames = + playbackQuality.totalVideoFrames; + } +} + +module.exports = { + test, + owner: "Network Team", + component: "netwerk", + name: "youtube-scroll", + description: "Measures quality of the video being played.", +}; diff --git a/netwerk/test/reftest/658949-1-ref.html b/netwerk/test/reftest/658949-1-ref.html new file mode 100644 index 0000000000..6e6d7e25f6 --- /dev/null +++ b/netwerk/test/reftest/658949-1-ref.html @@ -0,0 +1 @@ +<iframe src="data:text/html,ABC"></iframe> diff --git a/netwerk/test/reftest/658949-1.html b/netwerk/test/reftest/658949-1.html new file mode 100644 index 0000000000..f61c03a525 --- /dev/null +++ b/netwerk/test/reftest/658949-1.html @@ -0,0 +1 @@ +<iframe src="data:text/html,ABC#myRef"></iframe> diff --git a/netwerk/test/reftest/bug565432-1-ref.html b/netwerk/test/reftest/bug565432-1-ref.html new file mode 100644 index 0000000000..fade48a48d --- /dev/null +++ b/netwerk/test/reftest/bug565432-1-ref.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<title>Test newlines in href</title> +<ul> +<li><a href="about:blank">Link</a> +<li><a href="data:,test">Link</a> +<li><a href="file:///tmp/test">Link</a> +<li><a href="ftp://test.invalid/">Link</a> +<li><a href="gopher://test.invalid/">Link</a> +<li><a href="http://test.invalid/">Link</a> +<li><a href="ftp://test.invalid/%0a">Not Link</a> +</ul> diff --git a/netwerk/test/reftest/bug565432-1.html b/netwerk/test/reftest/bug565432-1.html new file mode 100644 index 0000000000..26a7270d06 --- /dev/null +++ b/netwerk/test/reftest/bug565432-1.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Test newlines in href</title> +<ul> +<li><a href=" +about:blank">Link</a> +<li><a href=" +data:,test">Link</a> +<li><a href=" +file:///tmp/test">Link</a> +<li><a href=" +ftp://test.invalid/">Link</a> +<li><a href=" +gopher://test.invalid/">Link</a> +<li><a href=" +http://test.invalid/">Link</a> +<li><a href="ftp://test.invalid/%0a">Not Link</a> +</ul> diff --git a/netwerk/test/reftest/reftest.list b/netwerk/test/reftest/reftest.list new file mode 100644 index 0000000000..98b5d4fb9a --- /dev/null +++ b/netwerk/test/reftest/reftest.list @@ -0,0 +1,2 @@ +== bug565432-1.html bug565432-1-ref.html +== 658949-1.html 658949-1-ref.html diff --git a/netwerk/test/unit/client-cert.p12 b/netwerk/test/unit/client-cert.p12 Binary files differnew file mode 100644 index 0000000000..80c8dad8a0 --- /dev/null +++ b/netwerk/test/unit/client-cert.p12 diff --git a/netwerk/test/unit/client-cert.p12.pkcs12spec b/netwerk/test/unit/client-cert.p12.pkcs12spec new file mode 100644 index 0000000000..548c1a6aa6 --- /dev/null +++ b/netwerk/test/unit/client-cert.p12.pkcs12spec @@ -0,0 +1,3 @@ +issuer:Test CA +subject:Test End-entity +extension:subjectAlternativeName:example.com diff --git a/netwerk/test/unit/data/cookies_v10.sqlite b/netwerk/test/unit/data/cookies_v10.sqlite Binary files differnew file mode 100644 index 0000000000..2301731f8e --- /dev/null +++ b/netwerk/test/unit/data/cookies_v10.sqlite diff --git a/netwerk/test/unit/data/image.png b/netwerk/test/unit/data/image.png Binary files differnew file mode 100644 index 0000000000..e0c5d3d6a1 --- /dev/null +++ b/netwerk/test/unit/data/image.png diff --git a/netwerk/test/unit/data/signed_win.exe b/netwerk/test/unit/data/signed_win.exe Binary files differnew file mode 100644 index 0000000000..de3bb40e84 --- /dev/null +++ b/netwerk/test/unit/data/signed_win.exe diff --git a/netwerk/test/unit/data/system_root.lnk b/netwerk/test/unit/data/system_root.lnk Binary files differnew file mode 100644 index 0000000000..e5885ce9a5 --- /dev/null +++ b/netwerk/test/unit/data/system_root.lnk diff --git a/netwerk/test/unit/data/test_psl.txt b/netwerk/test/unit/data/test_psl.txt new file mode 100644 index 0000000000..fa6e0d4cec --- /dev/null +++ b/netwerk/test/unit/data/test_psl.txt @@ -0,0 +1,98 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// null input. +checkPublicSuffix(null, null); +// Mixed case. +checkPublicSuffix('COM', null); +checkPublicSuffix('example.COM', 'example.com'); +checkPublicSuffix('WwW.example.COM', 'example.com'); +// Leading dot. +checkPublicSuffix('.com', null); +checkPublicSuffix('.example', null); +checkPublicSuffix('.example.com', null); +checkPublicSuffix('.example.example', null); +// Unlisted TLD. +checkPublicSuffix('example', null); +checkPublicSuffix('example.example', 'example.example'); +checkPublicSuffix('b.example.example', 'example.example'); +checkPublicSuffix('a.b.example.example', 'example.example'); +// Listed, but non-Internet, TLD. +//checkPublicSuffix('local', null); +//checkPublicSuffix('example.local', null); +//checkPublicSuffix('b.example.local', null); +//checkPublicSuffix('a.b.example.local', null); +// TLD with only 1 rule. +checkPublicSuffix('biz', null); +checkPublicSuffix('domain.biz', 'domain.biz'); +checkPublicSuffix('b.domain.biz', 'domain.biz'); +checkPublicSuffix('a.b.domain.biz', 'domain.biz'); +// TLD with some 2-level rules. +checkPublicSuffix('com', null); +checkPublicSuffix('example.com', 'example.com'); +checkPublicSuffix('b.example.com', 'example.com'); +checkPublicSuffix('a.b.example.com', 'example.com'); +checkPublicSuffix('uk.com', null); +checkPublicSuffix('example.uk.com', 'example.uk.com'); +checkPublicSuffix('b.example.uk.com', 'example.uk.com'); +checkPublicSuffix('a.b.example.uk.com', 'example.uk.com'); +checkPublicSuffix('test.ac', 'test.ac'); +// TLD with only 1 (wildcard) rule. +checkPublicSuffix('bd', null); +checkPublicSuffix('c.bd', null); +checkPublicSuffix('b.c.bd', 'b.c.bd'); +checkPublicSuffix('a.b.c.bd', 'b.c.bd'); +// More complex TLD. +checkPublicSuffix('jp', null); +checkPublicSuffix('test.jp', 'test.jp'); +checkPublicSuffix('www.test.jp', 'test.jp'); +checkPublicSuffix('ac.jp', null); +checkPublicSuffix('test.ac.jp', 'test.ac.jp'); +checkPublicSuffix('www.test.ac.jp', 'test.ac.jp'); +checkPublicSuffix('kyoto.jp', null); +checkPublicSuffix('test.kyoto.jp', 'test.kyoto.jp'); +checkPublicSuffix('ide.kyoto.jp', null); +checkPublicSuffix('b.ide.kyoto.jp', 'b.ide.kyoto.jp'); +checkPublicSuffix('a.b.ide.kyoto.jp', 'b.ide.kyoto.jp'); +checkPublicSuffix('c.kobe.jp', null); +checkPublicSuffix('b.c.kobe.jp', 'b.c.kobe.jp'); +checkPublicSuffix('a.b.c.kobe.jp', 'b.c.kobe.jp'); +checkPublicSuffix('city.kobe.jp', 'city.kobe.jp'); +checkPublicSuffix('www.city.kobe.jp', 'city.kobe.jp'); +// TLD with a wildcard rule and exceptions. +checkPublicSuffix('ck', null); +checkPublicSuffix('test.ck', null); +checkPublicSuffix('b.test.ck', 'b.test.ck'); +checkPublicSuffix('a.b.test.ck', 'b.test.ck'); +checkPublicSuffix('www.ck', 'www.ck'); +checkPublicSuffix('www.www.ck', 'www.ck'); +// US K12. +checkPublicSuffix('us', null); +checkPublicSuffix('test.us', 'test.us'); +checkPublicSuffix('www.test.us', 'test.us'); +checkPublicSuffix('ak.us', null); +checkPublicSuffix('test.ak.us', 'test.ak.us'); +checkPublicSuffix('www.test.ak.us', 'test.ak.us'); +checkPublicSuffix('k12.ak.us', null); +checkPublicSuffix('test.k12.ak.us', 'test.k12.ak.us'); +checkPublicSuffix('www.test.k12.ak.us', 'test.k12.ak.us'); +// IDN labels. +checkPublicSuffix('食狮.com.cn', '食狮.com.cn'); +checkPublicSuffix('食狮.公司.cn', '食狮.公司.cn'); +checkPublicSuffix('www.食狮.公司.cn', '食狮.公司.cn'); +checkPublicSuffix('shishi.公司.cn', 'shishi.公司.cn'); +checkPublicSuffix('公司.cn', null); +checkPublicSuffix('食狮.中国', '食狮.中国'); +checkPublicSuffix('www.食狮.中国', '食狮.中国'); +checkPublicSuffix('shishi.中国', 'shishi.中国'); +checkPublicSuffix('中国', null); +// Same as above, but punycoded. +checkPublicSuffix('xn--85x722f.com.cn', 'xn--85x722f.com.cn'); +checkPublicSuffix('xn--85x722f.xn--55qx5d.cn', 'xn--85x722f.xn--55qx5d.cn'); +checkPublicSuffix('www.xn--85x722f.xn--55qx5d.cn', 'xn--85x722f.xn--55qx5d.cn'); +checkPublicSuffix('shishi.xn--55qx5d.cn', 'shishi.xn--55qx5d.cn'); +checkPublicSuffix('xn--55qx5d.cn', null); +checkPublicSuffix('xn--85x722f.xn--fiqs8s', 'xn--85x722f.xn--fiqs8s'); +checkPublicSuffix('www.xn--85x722f.xn--fiqs8s', 'xn--85x722f.xn--fiqs8s'); +checkPublicSuffix('shishi.xn--fiqs8s', 'shishi.xn--fiqs8s'); +checkPublicSuffix('xn--fiqs8s', null); diff --git a/netwerk/test/unit/data/test_readline1.txt b/netwerk/test/unit/data/test_readline1.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/netwerk/test/unit/data/test_readline1.txt diff --git a/netwerk/test/unit/data/test_readline2.txt b/netwerk/test/unit/data/test_readline2.txt new file mode 100644 index 0000000000..67c3297611 --- /dev/null +++ b/netwerk/test/unit/data/test_readline2.txt @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/netwerk/test/unit/data/test_readline3.txt b/netwerk/test/unit/data/test_readline3.txt new file mode 100644 index 0000000000..decdc51878 --- /dev/null +++ b/netwerk/test/unit/data/test_readline3.txt @@ -0,0 +1,3 @@ + +
+
diff --git a/netwerk/test/unit/data/test_readline4.txt b/netwerk/test/unit/data/test_readline4.txt new file mode 100644 index 0000000000..ca25c36540 --- /dev/null +++ b/netwerk/test/unit/data/test_readline4.txt @@ -0,0 +1,3 @@ +1 +
23
456
+78901
diff --git a/netwerk/test/unit/data/test_readline5.txt b/netwerk/test/unit/data/test_readline5.txt new file mode 100644 index 0000000000..8463b7858e --- /dev/null +++ b/netwerk/test/unit/data/test_readline5.txt @@ -0,0 +1 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE
\ No newline at end of file diff --git a/netwerk/test/unit/data/test_readline6.txt b/netwerk/test/unit/data/test_readline6.txt new file mode 100644 index 0000000000..872c40afc4 --- /dev/null +++ b/netwerk/test/unit/data/test_readline6.txt @@ -0,0 +1 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE
diff --git a/netwerk/test/unit/data/test_readline7.txt b/netwerk/test/unit/data/test_readline7.txt new file mode 100644 index 0000000000..59ee122ce1 --- /dev/null +++ b/netwerk/test/unit/data/test_readline7.txt @@ -0,0 +1,2 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE
+
\ No newline at end of file diff --git a/netwerk/test/unit/data/test_readline8.txt b/netwerk/test/unit/data/test_readline8.txt new file mode 100644 index 0000000000..ff6fc09a4a --- /dev/null +++ b/netwerk/test/unit/data/test_readline8.txt @@ -0,0 +1 @@ +zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzSxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE
\ No newline at end of file diff --git a/netwerk/test/unit/head_cache.js b/netwerk/test/unit/head_cache.js new file mode 100644 index 0000000000..7ec0e11f97 --- /dev/null +++ b/netwerk/test/unit/head_cache.js @@ -0,0 +1,137 @@ +"use strict"; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +function evict_cache_entries(where) { + var clearDisk = !where || where == "disk" || where == "all"; + var clearMem = !where || where == "memory" || where == "all"; + + var storage; + + if (clearMem) { + storage = Services.cache2.memoryCacheStorage( + Services.loadContextInfo.default + ); + storage.asyncEvictStorage(null); + } + + if (clearDisk) { + storage = Services.cache2.diskCacheStorage( + Services.loadContextInfo.default + ); + storage.asyncEvictStorage(null); + } +} + +function createURI(urispec) { + return Services.io.newURI(urispec); +} + +function getCacheStorage(where, lci) { + if (!lci) { + lci = Services.loadContextInfo.default; + } + switch (where) { + case "disk": + return Services.cache2.diskCacheStorage(lci); + case "memory": + return Services.cache2.memoryCacheStorage(lci); + case "pin": + return Services.cache2.pinningCacheStorage(lci); + } + return null; +} + +function asyncOpenCacheEntry(key, where, flags, lci, callback) { + key = createURI(key); + + function CacheListener() {} + CacheListener.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), + + onCacheEntryCheck(entry) { + if (typeof callback === "object") { + return callback.onCacheEntryCheck(entry); + } + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + + onCacheEntryAvailable(entry, isnew, status) { + if (typeof callback === "object") { + // Root us at the callback + callback.__cache_listener_root = this; + callback.onCacheEntryAvailable(entry, isnew, status); + } else { + callback(status, entry); + } + }, + + run() { + var storage = getCacheStorage(where, lci); + storage.asyncOpenURI(key, "", flags, this); + }, + }; + + new CacheListener().run(); +} + +function syncWithCacheIOThread(callback, force) { + if (force) { + asyncOpenCacheEntry( + "http://nonexistententry/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + function (status, entry) { + Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + callback(); + } + ); + } else { + callback(); + } +} + +function get_device_entry_count(where, lci, continuation) { + var storage = getCacheStorage(where, lci); + if (!storage) { + continuation(-1, 0); + return; + } + + var visitor = { + onCacheStorageInfo(entryCount, consumption) { + executeSoon(function () { + continuation(entryCount, consumption); + }); + }, + }; + + // get the device entry count + storage.asyncVisitStorage(visitor, false); +} + +function asyncCheckCacheEntryPresence(key, where, shouldExist, continuation) { + asyncOpenCacheEntry( + key, + where, + Ci.nsICacheStorage.OPEN_READONLY, + null, + function (status, entry) { + if (shouldExist) { + dump("TEST-INFO | checking cache key " + key + " exists @ " + where); + Assert.equal(status, Cr.NS_OK); + Assert.ok(!!entry); + } else { + dump( + "TEST-INFO | checking cache key " + key + " doesn't exist @ " + where + ); + Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + Assert.equal(null, entry); + } + continuation(); + } + ); +} diff --git a/netwerk/test/unit/head_cache2.js b/netwerk/test/unit/head_cache2.js new file mode 100644 index 0000000000..f7c865872a --- /dev/null +++ b/netwerk/test/unit/head_cache2.js @@ -0,0 +1,429 @@ +/* import-globals-from head_cache.js */ +/* import-globals-from head_channels.js */ + +"use strict"; + +var callbacks = []; + +// Expect an existing entry +const NORMAL = 0; +// Expect a new entry +const NEW = 1 << 0; +// Return early from onCacheEntryCheck and set the callback to state it expects onCacheEntryCheck to happen +const NOTVALID = 1 << 1; +// Throw from onCacheEntryAvailable +const THROWAVAIL = 1 << 2; +// Open entry for reading-only +const READONLY = 1 << 3; +// Expect the entry to not be found +const NOTFOUND = 1 << 4; +// Return ENTRY_NEEDS_REVALIDATION from onCacheEntryCheck +const REVAL = 1 << 5; +// Return ENTRY_PARTIAL from onCacheEntryCheck, in combo with NEW or RECREATE bypasses check for emptiness of the entry +const PARTIAL = 1 << 6; +// Expect the entry is doomed, i.e. the output stream should not be possible to open +const DOOMED = 1 << 7; +// Don't trigger the go-on callback until the entry is written +const WAITFORWRITE = 1 << 8; +// Don't write data (i.e. don't open output stream) +const METAONLY = 1 << 9; +// Do recreation of an existing cache entry +const RECREATE = 1 << 10; +// Do not give me the entry +const NOTWANTED = 1 << 11; +// Tell the cache to wait for the entry to be completely written first +const COMPLETE = 1 << 12; +// Don't write meta/data and don't set valid in the callback, consumer will do it manually +const DONTFILL = 1 << 13; +// Used in combination with METAONLY, don't call setValid() on the entry after metadata has been set +const DONTSETVALID = 1 << 14; +// Notify before checking the data, useful for proper callback ordering checks +const NOTIFYBEFOREREAD = 1 << 15; +// It's allowed to not get an existing entry (result of opening is undetermined) +const MAYBE_NEW = 1 << 16; + +var log_c2 = true; +function LOG_C2(o, m) { + if (!log_c2) { + return; + } + if (!m) { + dump("TEST-INFO | CACHE2: " + o + "\n"); + } else { + dump( + "TEST-INFO | CACHE2: callback #" + + o.order + + "(" + + (o.workingData ? o.workingData.substr(0, 10) : "---") + + ") " + + m + + "\n" + ); + } +} + +function pumpReadStream(inputStream, goon) { + if (inputStream.isNonBlocking()) { + // non-blocking stream, must read via pump + var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance( + Ci.nsIInputStreamPump + ); + pump.init(inputStream, 0, 0, true); + let data = ""; + pump.asyncRead({ + onStartRequest(aRequest) {}, + onDataAvailable(aRequest, aInputStream, aOffset, aCount) { + var wrapper = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + wrapper.init(aInputStream); + var str = wrapper.read(wrapper.available()); + LOG_C2("reading data '" + str.substring(0, 5) + "'"); + data += str; + }, + onStopRequest(aRequest, aStatusCode) { + LOG_C2("done reading data: " + aStatusCode); + Assert.equal(aStatusCode, Cr.NS_OK); + goon(data); + }, + }); + } else { + // blocking stream + let data = read_stream(inputStream, inputStream.available()); + goon(data); + } +} + +OpenCallback.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), + onCacheEntryCheck(entry) { + LOG_C2(this, "onCacheEntryCheck"); + Assert.ok(!this.onCheckPassed); + this.onCheckPassed = true; + + if (this.behavior & NOTVALID) { + LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED"); + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + } + + if (this.behavior & NOTWANTED) { + LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NOT_WANTED"); + return Ci.nsICacheEntryOpenCallback.ENTRY_NOT_WANTED; + } + + Assert.equal(entry.getMetaDataElement("meto"), this.workingMetadata); + + // check for sane flag combination + Assert.notEqual(this.behavior & (REVAL | PARTIAL), REVAL | PARTIAL); + + if (this.behavior & (REVAL | PARTIAL)) { + LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NEEDS_REVALIDATION"); + return Ci.nsICacheEntryOpenCallback.ENTRY_NEEDS_REVALIDATION; + } + + if (this.behavior & COMPLETE) { + LOG_C2( + this, + "onCacheEntryCheck DONE, return RECHECK_AFTER_WRITE_FINISHED" + ); + // Specific to the new backend because of concurrent read/write: + // when a consumer returns RECHECK_AFTER_WRITE_FINISHED from onCacheEntryCheck + // the cache calls this callback again after the entry write has finished. + // This gives the consumer a chance to recheck completeness of the entry + // again. + // Thus, we reset state as onCheck would have never been called. + this.onCheckPassed = false; + // Don't return RECHECK_AFTER_WRITE_FINISHED on second call of onCacheEntryCheck. + this.behavior &= ~COMPLETE; + return Ci.nsICacheEntryOpenCallback.RECHECK_AFTER_WRITE_FINISHED; + } + + LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED"); + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + onCacheEntryAvailable(entry, isnew, status) { + if (this.behavior & MAYBE_NEW && isnew) { + this.behavior |= NEW; + } + + LOG_C2(this, "onCacheEntryAvailable, " + this.behavior); + Assert.ok(!this.onAvailPassed); + this.onAvailPassed = true; + + Assert.equal(isnew, !!(this.behavior & NEW)); + + if (this.behavior & (NOTFOUND | NOTWANTED)) { + Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + Assert.ok(!entry); + if (this.behavior & THROWAVAIL) { + this.throwAndNotify(entry); + } + this.goon(entry); + } else if (this.behavior & (NEW | RECREATE)) { + Assert.ok(!!entry); + + if (this.behavior & RECREATE) { + entry = entry.recreate(); + Assert.ok(!!entry); + } + + if (this.behavior & THROWAVAIL) { + this.throwAndNotify(entry); + } + + if (!(this.behavior & WAITFORWRITE)) { + this.goon(entry); + } + + if (!(this.behavior & PARTIAL)) { + try { + entry.getMetaDataElement("meto"); + Assert.ok(false); + } catch (ex) {} + } + + if (this.behavior & DONTFILL) { + Assert.equal(false, this.behavior & WAITFORWRITE); + return; + } + + let self = this; + executeSoon(function () { + // emulate network latency + entry.setMetaDataElement("meto", self.workingMetadata); + entry.metaDataReady(); + if (self.behavior & METAONLY) { + // Since forcing GC/CC doesn't trigger OnWriterClosed, we have to set the entry valid manually :( + if (!(self.behavior & DONTSETVALID)) { + entry.setValid(); + } + + entry.close(); + if (self.behavior & WAITFORWRITE) { + self.goon(entry); + } + + return; + } + executeSoon(function () { + // emulate more network latency + if (self.behavior & DOOMED) { + LOG_C2(self, "checking doom state"); + try { + let os = entry.openOutputStream(0, -1); + // Unfortunately, in the undetermined state we cannot even check whether the entry + // is actually doomed or not. + os.close(); + Assert.ok(!!(self.behavior & MAYBE_NEW)); + } catch (ex) { + Assert.ok(true); + } + if (self.behavior & WAITFORWRITE) { + self.goon(entry); + } + return; + } + + var offset = self.behavior & PARTIAL ? entry.dataSize : 0; + LOG_C2(self, "openOutputStream @ " + offset); + let os = entry.openOutputStream(offset, -1); + LOG_C2(self, "writing data"); + var wrt = os.write(self.workingData, self.workingData.length); + Assert.equal(wrt, self.workingData.length); + os.close(); + if (self.behavior & WAITFORWRITE) { + self.goon(entry); + } + + entry.close(); + }); + }); + } else { + // NORMAL + Assert.ok(!!entry); + Assert.equal(entry.getMetaDataElement("meto"), this.workingMetadata); + if (this.behavior & THROWAVAIL) { + this.throwAndNotify(entry); + } + if (this.behavior & NOTIFYBEFOREREAD) { + this.goon(entry, true); + } + + let self = this; + pumpReadStream(entry.openInputStream(0), function (data) { + Assert.equal(data, self.workingData); + self.onDataCheckPassed = true; + LOG_C2(self, "entry read done"); + self.goon(entry); + entry.close(); + }); + } + }, + selfCheck() { + LOG_C2(this, "selfCheck"); + + Assert.ok(this.onCheckPassed || this.behavior & MAYBE_NEW); + Assert.ok(this.onAvailPassed); + Assert.ok(this.onDataCheckPassed || this.behavior & MAYBE_NEW); + }, + throwAndNotify(entry) { + LOG_C2(this, "Throwing"); + var self = this; + executeSoon(function () { + LOG_C2(self, "Notifying"); + self.goon(entry); + }); + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + }, +}; + +function OpenCallback(behavior, workingMetadata, workingData, goon) { + this.behavior = behavior; + this.workingMetadata = workingMetadata; + this.workingData = workingData; + this.goon = goon; + this.onCheckPassed = + (!!(behavior & (NEW | RECREATE)) || !workingMetadata) && + !(behavior & NOTVALID); + this.onAvailPassed = false; + this.onDataCheckPassed = + !!(behavior & (NEW | RECREATE | NOTWANTED)) || !workingMetadata; + callbacks.push(this); + this.order = callbacks.length; +} + +VisitCallback.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + onCacheStorageInfo(num, consumption) { + LOG_C2(this, "onCacheStorageInfo: num=" + num + ", size=" + consumption); + Assert.equal(this.num, num); + Assert.equal(this.consumption, consumption); + if (!this.entries) { + this.notify(); + } + }, + onCacheEntryInfo( + aURI, + aIdEnhance, + aDataSize, + aAltDataSize, + aFetchCount, + aLastModifiedTime, + aExpirationTime, + aPinned, + aInfo + ) { + var key = (aIdEnhance ? aIdEnhance + ":" : "") + aURI.asciiSpec; + LOG_C2(this, "onCacheEntryInfo: key=" + key); + + function findCacheIndex(element) { + if (typeof element === "string") { + return element === key; + } else if (typeof element === "object") { + return ( + element.uri === key && + element.lci.isAnonymous === aInfo.isAnonymous && + ChromeUtils.isOriginAttributesEqual( + element.lci.originAttributes, + aInfo.originAttributes + ) + ); + } + + return false; + } + + Assert.ok(!!this.entries); + + var index = this.entries.findIndex(findCacheIndex); + Assert.ok(index > -1); + + this.entries.splice(index, 1); + }, + onCacheEntryVisitCompleted() { + LOG_C2(this, "onCacheEntryVisitCompleted"); + if (this.entries) { + Assert.equal(this.entries.length, 0); + } + this.notify(); + }, + notify() { + Assert.ok(!!this.goon); + var goon = this.goon; + this.goon = null; + executeSoon(goon); + }, + selfCheck() { + Assert.ok(!this.entries || !this.entries.length); + }, +}; + +function VisitCallback(num, consumption, entries, goon) { + this.num = num; + this.consumption = consumption; + this.entries = entries; + this.goon = goon; + callbacks.push(this); + this.order = callbacks.length; +} + +EvictionCallback.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsICacheEntryDoomCallback"]), + onCacheEntryDoomed(result) { + Assert.equal(this.expectedSuccess, result == Cr.NS_OK); + this.goon(); + }, + selfCheck() {}, +}; + +function EvictionCallback(success, goon) { + this.expectedSuccess = success; + this.goon = goon; + callbacks.push(this); + this.order = callbacks.length; +} + +MultipleCallbacks.prototype = { + fired() { + if (--this.pending == 0) { + var self = this; + if (this.delayed) { + executeSoon(function () { + self.goon(); + }); + } else { + this.goon(); + } + } + }, + add() { + ++this.pending; + }, +}; + +function MultipleCallbacks(number, goon, delayed) { + this.pending = number; + this.goon = goon; + this.delayed = delayed; +} + +function wait_for_cache_index(continue_func) { + // This callback will not fire before the index is in the ready state. nsICacheStorage.exists() will + // no longer throw after this point. + Services.cache2.asyncGetDiskConsumption({ + onNetworkCacheDiskConsumption() { + continue_func(); + }, + // eslint-disable-next-line mozilla/use-chromeutils-generateqi + QueryInterface() { + return this; + }, + }); +} + +function finish_cache2_test() { + callbacks.forEach(function (callback, index) { + callback.selfCheck(); + }); + do_test_finished(); +} diff --git a/netwerk/test/unit/head_channels.js b/netwerk/test/unit/head_channels.js new file mode 100644 index 0000000000..791284bdce --- /dev/null +++ b/netwerk/test/unit/head_channels.js @@ -0,0 +1,527 @@ +/** + * Read count bytes from stream and return as a String object + */ + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ + +function read_stream(stream, count) { + /* assume stream has non-ASCII data */ + var wrapper = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + wrapper.setInputStream(stream); + /* JS methods can be called with a maximum of 65535 arguments, and input + streams don't have to return all the data they make .available() when + asked to .read() that number of bytes. */ + var data = []; + while (count > 0) { + var bytes = wrapper.readByteArray(Math.min(65535, count)); + data.push(String.fromCharCode.apply(null, bytes)); + count -= bytes.length; + if (!bytes.length) { + do_throw("Nothing read from input stream!"); + } + } + return data.join(""); +} + +const CL_EXPECT_FAILURE = 0x1; +const CL_EXPECT_GZIP = 0x2; +const CL_EXPECT_3S_DELAY = 0x4; +const CL_SUSPEND = 0x8; +const CL_ALLOW_UNKNOWN_CL = 0x10; +const CL_EXPECT_LATE_FAILURE = 0x20; +const CL_FROM_CACHE = 0x40; // Response must be from the cache +const CL_NOT_FROM_CACHE = 0x80; // Response must NOT be from the cache +const CL_IGNORE_CL = 0x100; // don't bother to verify the content-length +const CL_IGNORE_DELAYS = 0x200; // don't throw if channel returns after a long delay + +const SUSPEND_DELAY = 3000; + +/** + * A stream listener that calls a callback function with a specified + * context and the received data when the channel is loaded. + * + * Signature of the closure: + * void closure(in nsIRequest request, in ACString data, in JSObject context); + * + * This listener makes sure that various parts of the channel API are + * implemented correctly and that the channel's status is a success code + * (you can pass CL_EXPECT_FAILURE or CL_EXPECT_LATE_FAILURE as flags + * to allow a failure code) + * + * Note that it also requires a valid content length on the channel and + * is thus not fully generic. + */ +function ChannelListener(closure, ctx, flags) { + this._closure = closure; + this._closurectx = ctx; + this._flags = flags; + this._isFromCache = false; + this._cacheEntryId = undefined; +} +ChannelListener.prototype = { + _closure: null, + _closurectx: null, + _buffer: "", + _got_onstartrequest: false, + _got_onstoprequest: false, + _contentLen: -1, + _lastEvent: 0, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + try { + if (this._got_onstartrequest) { + do_throw("Got second onStartRequest event!"); + } + this._got_onstartrequest = true; + this._lastEvent = Date.now(); + + try { + this._isFromCache = request + .QueryInterface(Ci.nsICacheInfoChannel) + .isFromCache(); + } catch (e) {} + + var thrown = false; + try { + this._cacheEntryId = request + .QueryInterface(Ci.nsICacheInfoChannel) + .getCacheEntryId(); + } catch (e) { + thrown = true; + } + if (this._isFromCache && thrown) { + do_throw("Should get a CacheEntryId"); + } else if (!this._isFromCache && !thrown) { + do_throw("Shouldn't get a CacheEntryId"); + } + + request.QueryInterface(Ci.nsIChannel); + try { + this._contentLen = request.contentLength; + } catch (ex) { + if (!(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL))) { + do_throw("Could not get contentLength"); + } + } + if (!request.isPending()) { + do_throw("request reports itself as not pending from onStartRequest!"); + } + if ( + this._contentLen == -1 && + !(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL)) + ) { + do_throw("Content length is unknown in onStartRequest!"); + } + + if (this._flags & CL_FROM_CACHE) { + request.QueryInterface(Ci.nsICachingChannel); + if (!request.isFromCache()) { + do_throw("Response is not from the cache (CL_FROM_CACHE)"); + } + } + if (this._flags & CL_NOT_FROM_CACHE) { + request.QueryInterface(Ci.nsICachingChannel); + if (request.isFromCache()) { + do_throw("Response is from the cache (CL_NOT_FROM_CACHE)"); + } + } + + if (this._flags & CL_SUSPEND) { + request.suspend(); + do_timeout(SUSPEND_DELAY, function () { + request.resume(); + }); + } + } catch (ex) { + do_throw("Error in onStartRequest: " + ex); + } + }, + + onDataAvailable(request, stream, offset, count) { + try { + let current = Date.now(); + + if (!this._got_onstartrequest) { + do_throw("onDataAvailable without onStartRequest event!"); + } + if (this._got_onstoprequest) { + do_throw("onDataAvailable after onStopRequest event!"); + } + if (!request.isPending()) { + do_throw("request reports itself as not pending from onDataAvailable!"); + } + if (this._flags & CL_EXPECT_FAILURE) { + do_throw("Got data despite expecting a failure"); + } + + if ( + !(this._flags & CL_IGNORE_DELAYS) && + current - this._lastEvent >= SUSPEND_DELAY && + !(this._flags & CL_EXPECT_3S_DELAY) + ) { + do_throw("Data received after significant unexpected delay"); + } else if ( + current - this._lastEvent < SUSPEND_DELAY && + this._flags & CL_EXPECT_3S_DELAY + ) { + do_throw("Data received sooner than expected"); + } else if ( + current - this._lastEvent >= SUSPEND_DELAY && + this._flags & CL_EXPECT_3S_DELAY + ) { + this._flags &= ~CL_EXPECT_3S_DELAY; + } // No more delays expected + + this._buffer = this._buffer.concat(read_stream(stream, count)); + this._lastEvent = current; + } catch (ex) { + do_throw("Error in onDataAvailable: " + ex); + } + }, + + onStopRequest(request, status) { + try { + var success = Components.isSuccessCode(status); + if (!this._got_onstartrequest) { + do_throw("onStopRequest without onStartRequest event!"); + } + if (this._got_onstoprequest) { + do_throw("Got second onStopRequest event!"); + } + this._got_onstoprequest = true; + if ( + this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE) && + success + ) { + do_throw( + "Should have failed to load URL (status is " + + status.toString(16) + + ")" + ); + } else if ( + !(this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE)) && + !success + ) { + do_throw("Failed to load URL: " + status.toString(16)); + } + if (status != request.status) { + do_throw("request.status does not match status arg to onStopRequest!"); + } + if (request.isPending()) { + do_throw("request reports itself as pending from onStopRequest!"); + } + if ( + !( + this._flags & + (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE | CL_IGNORE_CL) + ) && + !(this._flags & CL_EXPECT_GZIP) && + this._contentLen != -1 + ) { + Assert.equal(this._buffer.length, this._contentLen); + } + } catch (ex) { + do_throw("Error in onStopRequest: " + ex); + } + try { + this._closure( + request, + this._buffer, + this._closurectx, + this._isFromCache, + this._cacheEntryId + ); + this._closurectx = null; + } catch (ex) { + do_throw("Error in closure function: " + ex); + } + }, +}; + +var ES_ABORT_REDIRECT = 0x01; + +function ChannelEventSink(flags) { + this._flags = flags; +} + +ChannelEventSink.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface(iid) { + if (iid.equals(Ci.nsIChannelEventSink)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { + if (this._flags & ES_ABORT_REDIRECT) { + throw Components.Exception("", Cr.NS_BINDING_ABORTED); + } + + callback.onRedirectVerifyCallback(Cr.NS_OK); + }, +}; + +/** + * A helper class to construct origin attributes. + */ +function OriginAttributes(inIsolatedMozBrowser, privateId) { + this.inIsolatedMozBrowser = inIsolatedMozBrowser; + this.privateBrowsingId = privateId; +} +OriginAttributes.prototype = { + inIsolatedMozBrowser: false, + privateBrowsingId: 0, +}; + +function readFile(file) { + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(file, -1, 0, 0); + let data = NetUtil.readInputStreamToString(fstream, fstream.available()); + fstream.close(); + return data; +} + +function addCertFromFile(certdb, filename, trustString) { + let certFile = do_get_file(filename, false); + let pem = readFile(certFile) + .replace(/-----BEGIN CERTIFICATE-----/, "") + .replace(/-----END CERTIFICATE-----/, "") + .replace(/[\r\n]/g, ""); + certdb.addCertFromBase64(pem, trustString); +} + +// Helper code to test nsISerializable +function serialize_to_escaped_string(obj) { + let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIObjectOutputStream + ); + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(false, false, 0, 0xffffffff, null); + objectOutStream.setOutputStream(pipe.outputStream); + objectOutStream.writeCompoundObject(obj, Ci.nsISupports, true); + objectOutStream.close(); + + let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIObjectInputStream + ); + objectInStream.setInputStream(pipe.inputStream); + let data = []; + // This reads all the data from the stream until an error occurs. + while (true) { + try { + let bytes = objectInStream.readByteArray(1); + data.push(String.fromCharCode.apply(null, bytes)); + } catch (e) { + break; + } + } + return escape(data.join("")); +} + +function deserialize_from_escaped_string(str) { + let payload = unescape(str); + let data = []; + let i = 0; + while (i < payload.length) { + data.push(payload.charCodeAt(i++)); + } + + let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIObjectOutputStream + ); + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(false, false, 0, 0xffffffff, null); + objectOutStream.setOutputStream(pipe.outputStream); + objectOutStream.writeByteArray(data); + objectOutStream.close(); + + let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIObjectInputStream + ); + objectInStream.setInputStream(pipe.inputStream); + return objectInStream.readObject(true); +} + +async function asyncStartTLSTestServer( + serverBinName, + certsPath, + addDefaultRoot = true +) { + const { HttpServer } = ChromeUtils.import( + "resource://testing-common/httpd.js" + ); + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + // The trusted CA that is typically used for "good" certificates. + if (addDefaultRoot) { + addCertFromFile(certdb, `${certsPath}/test-ca.pem`, "CTu,u,u"); + } + + const CALLBACK_PORT = 8444; + + let greBinDir = Services.dirsvc.get("GreBinD", Ci.nsIFile); + Services.env.set("DYLD_LIBRARY_PATH", greBinDir.path); + // TODO(bug 1107794): Android libraries are in /data/local/xpcb, but "GreBinD" + // does not return this path on Android, so hard code it here. + Services.env.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb"); + Services.env.set("MOZ_TLS_SERVER_DEBUG_LEVEL", "3"); + Services.env.set("MOZ_TLS_SERVER_CALLBACK_PORT", CALLBACK_PORT); + + let httpServer = new HttpServer(); + let serverReady = new Promise(resolve => { + httpServer.registerPathHandler( + "/", + function handleServerCallback(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + aResponse.setHeader("Content-Type", "text/plain"); + let responseBody = "OK!"; + aResponse.bodyOutputStream.write(responseBody, responseBody.length); + executeSoon(function () { + httpServer.stop(resolve); + }); + } + ); + httpServer.start(CALLBACK_PORT); + }); + + let serverBin = _getBinaryUtil(serverBinName); + let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(serverBin); + let certDir = do_get_file(certsPath, false); + Assert.ok(certDir.exists(), `certificate folder (${certsPath}) should exist`); + // Using "sql:" causes the SQL DB to be used so we can run tests on Android. + process.run(false, ["sql:" + certDir.path, Services.appinfo.processID], 2); + + registerCleanupFunction(function () { + process.kill(); + }); + + await serverReady; +} + +function _getBinaryUtil(binaryUtilName) { + let utilBin = Services.dirsvc.get("GreD", Ci.nsIFile); + // On macOS, GreD is .../Contents/Resources, and most binary utilities + // are located there, but certutil is in GreBinD (or .../Contents/MacOS), + // so we have to change the path accordingly. + if (binaryUtilName === "certutil") { + utilBin = Services.dirsvc.get("GreBinD", Ci.nsIFile); + } + utilBin.append(binaryUtilName + mozinfo.bin_suffix); + // If we're testing locally, the above works. If not, the server executable + // is in another location. + if (!utilBin.exists()) { + utilBin = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + while (utilBin.path.includes("xpcshell")) { + utilBin = utilBin.parent; + } + utilBin.append("bin"); + utilBin.append(binaryUtilName + mozinfo.bin_suffix); + } + // But maybe we're on Android, where binaries are in /data/local/xpcb. + if (!utilBin.exists()) { + utilBin.initWithPath("/data/local/xpcb/"); + utilBin.append(binaryUtilName); + } + Assert.ok(utilBin.exists(), `Binary util ${binaryUtilName} should exist`); + return utilBin; +} + +function promiseAsyncOpen(chan) { + return new Promise(resolve => { + chan.asyncOpen( + new ChannelListener((req, buf, ctx, isCache, cacheId) => { + resolve({ req, buf, ctx, isCache, cacheId }); + }) + ); + }); +} + +function hexStringToBytes(hex) { + let bytes = []; + for (let hexByteStr of hex.split(/(..)/)) { + if (hexByteStr.length) { + bytes.push(parseInt(hexByteStr, 16)); + } + } + return bytes; +} + +function stringToBytes(str) { + return Array.from(str, chr => chr.charCodeAt(0)); +} + +function BinaryHttpResponse(status, headerNames, headerValues, content) { + this.status = status; + this.headerNames = headerNames; + this.headerValues = headerValues; + this.content = content; +} + +BinaryHttpResponse.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpResponse"]), +}; + +function bytesToString(bytes) { + return String.fromCharCode.apply(null, bytes); +} + +function check_http_info(request, expected_httpVersion, expected_proxy) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + + request.QueryInterface(Ci.nsIProxiedChannel); + var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + + Assert.equal(expected_httpVersion, httpVersion); + if (expected_proxy) { + Assert.equal(httpProxyConnectResponseCode, 200); + } else { + Assert.equal(httpProxyConnectResponseCode, -1); + } +} + +function makeHTTPChannel(url, with_proxy) { + function createPrincipal(uri) { + var ssm = Services.scriptSecurityManager; + try { + return ssm.createContentPrincipal(Services.io.newURI(uri), {}); + } catch (e) { + return null; + } + } + + if (with_proxy) { + return Services.io + .newChannelFromURIWithProxyFlags( + Services.io.newURI(url), + null, + Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL, + null, + createPrincipal(url), + createPrincipal(url), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, + Ci.nsIContentPolicy.TYPE_OTHER + ) + .QueryInterface(Ci.nsIHttpChannel); + } + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} diff --git a/netwerk/test/unit/head_cookies.js b/netwerk/test/unit/head_cookies.js new file mode 100644 index 0000000000..223ea90120 --- /dev/null +++ b/netwerk/test/unit/head_cookies.js @@ -0,0 +1,955 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from head_cache.js */ + +"use strict"; + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +// Don't pick up default permissions from profile. +Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); + +CookieXPCShellUtils.init(this); + +function do_check_throws(f, result, stack) { + if (!stack) { + stack = Components.stack.caller; + } + + try { + f(); + } catch (exc) { + if (exc.result == result) { + return; + } + do_throw("expected result " + result + ", caught " + exc, stack); + } + do_throw("expected result " + result + ", none thrown", stack); +} + +// Helper to step a generator function and catch a StopIteration exception. +function do_run_generator(generator) { + try { + generator.next(); + } catch (e) { + do_throw("caught exception " + e, Components.stack.caller); + } +} + +// Helper to finish a generator function test. +function do_finish_generator_test(generator) { + executeSoon(function () { + generator.return(); + do_test_finished(); + }); +} + +function _observer(generator, topic) { + Services.obs.addObserver(this, topic); + + this.generator = generator; + this.topic = topic; +} + +_observer.prototype = { + observe(subject, topic, data) { + Assert.equal(this.topic, topic); + + Services.obs.removeObserver(this, this.topic); + + // Continue executing the generator function. + if (this.generator) { + do_run_generator(this.generator); + } + + this.generator = null; + this.topic = null; + }, +}; + +// Close the cookie database. If a generator is supplied, it will be invoked +// once the close is complete. +function do_close_profile(generator) { + // Register an observer for db close. + new _observer(generator, "cookie-db-closed"); + + // Close the db. + let service = Services.cookies.QueryInterface(Ci.nsIObserver); + service.observe(null, "profile-before-change", null); +} + +function _promise_observer(topic) { + Services.obs.addObserver(this, topic); + + this.topic = topic; + return new Promise(resolve => (this.resolve = resolve)); +} + +_promise_observer.prototype = { + observe(subject, topic, data) { + Assert.equal(this.topic, topic); + + Services.obs.removeObserver(this, this.topic); + if (this.resolve) { + this.resolve(); + } + + this.resolve = null; + this.topic = null; + }, +}; + +// Close the cookie database. And resolve a promise. +function promise_close_profile() { + // Register an observer for db close. + let promise = new _promise_observer("cookie-db-closed"); + + // Close the db. + let service = Services.cookies.QueryInterface(Ci.nsIObserver); + service.observe(null, "profile-before-change", null); + + return promise; +} + +// Load the cookie database. +function promise_load_profile() { + // Register an observer for read completion. + let promise = new _promise_observer("cookie-db-read"); + + // Load the profile. + let service = Services.cookies.QueryInterface(Ci.nsIObserver); + service.observe(null, "profile-do-change", ""); + + return promise; +} + +// Load the cookie database. If a generator is supplied, it will be invoked +// once the load is complete. +function do_load_profile(generator) { + // Register an observer for read completion. + new _observer(generator, "cookie-db-read"); + + // Load the profile. + let service = Services.cookies.QueryInterface(Ci.nsIObserver); + service.observe(null, "profile-do-change", ""); +} + +// Set a single session cookie using http and test the cookie count +// against 'expected' +function do_set_single_http_cookie(uri, channel, expected) { + Services.cookies.setCookieStringFromHttp(uri, "foo=bar", channel); + Assert.equal(Services.cookies.countCookiesFromHost(uri.host), expected); +} + +// Set two cookies; via document.channel and via http request. +async function do_set_cookies(uri, channel, session, expected) { + let suffix = session ? "" : "; max-age=1000"; + + // via document.cookie + const thirdPartyUrl = "http://third.com/"; + const contentPage = await CookieXPCShellUtils.loadContentPage(thirdPartyUrl); + await contentPage.spawn( + [ + { + cookie: "can=has" + suffix, + url: uri.spec, + }, + ], + async obj => { + // eslint-disable-next-line no-undef + await new content.Promise(resolve => { + // eslint-disable-next-line no-undef + const ifr = content.document.createElement("iframe"); + // eslint-disable-next-line no-undef + content.document.body.appendChild(ifr); + ifr.src = obj.url; + ifr.onload = () => { + ifr.contentDocument.cookie = obj.cookie; + resolve(); + }; + }); + } + ); + await contentPage.close(); + + Assert.equal(Services.cookies.countCookiesFromHost(uri.host), expected[0]); + + // via http request + Services.cookies.setCookieStringFromHttp(uri, "hot=dog" + suffix, channel); + Assert.equal(Services.cookies.countCookiesFromHost(uri.host), expected[1]); +} + +function do_count_cookies() { + return Services.cookies.cookies.length; +} + +// Helper object to store cookie data. +function Cookie( + name, + value, + host, + path, + expiry, + lastAccessed, + creationTime, + isSession, + isSecure, + isHttpOnly, + inBrowserElement = false, + originAttributes = {}, + sameSite = Ci.nsICookie.SAMESITE_NONE, + rawSameSite = Ci.nsICookie.SAMESITE_NONE, + schemeMap = Ci.nsICookie.SCHEME_UNSET +) { + this.name = name; + this.value = value; + this.host = host; + this.path = path; + this.expiry = expiry; + this.lastAccessed = lastAccessed; + this.creationTime = creationTime; + this.isSession = isSession; + this.isSecure = isSecure; + this.isHttpOnly = isHttpOnly; + this.inBrowserElement = inBrowserElement; + this.originAttributes = originAttributes; + this.sameSite = sameSite; + this.rawSameSite = rawSameSite; + this.schemeMap = schemeMap; + + let strippedHost = host.charAt(0) == "." ? host.slice(1) : host; + + try { + this.baseDomain = Services.eTLD.getBaseDomainFromHost(strippedHost); + } catch (e) { + if ( + e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS || + e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS + ) { + this.baseDomain = strippedHost; + } + } +} + +// Object representing a database connection and associated statements. The +// implementation varies depending on schema version. +function CookieDatabaseConnection(file, schema) { + // Manually generate a cookies.sqlite file with appropriate rows, columns, + // and schema version. If it already exists, just set up our statements. + let exists = file.exists(); + + this.db = Services.storage.openDatabase(file); + this.schema = schema; + if (!exists) { + this.db.schemaVersion = schema; + } + + switch (schema) { + case 1: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER)" + ); + } + + this.stmtInsert = this.db.createStatement( + "INSERT INTO moz_cookies ( \ + id, \ + name, \ + value, \ + host, \ + path, \ + expiry, \ + isSecure, \ + isHttpOnly) \ + VALUES ( \ + :id, \ + :name, \ + :value, \ + :host, \ + :path, \ + :expiry, \ + :isSecure, \ + :isHttpOnly)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies WHERE id = :id" + ); + + break; + } + + case 2: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + lastAccessed INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER)" + ); + } + + this.stmtInsert = this.db.createStatement( + "INSERT OR REPLACE INTO moz_cookies ( \ + id, \ + name, \ + value, \ + host, \ + path, \ + expiry, \ + lastAccessed, \ + isSecure, \ + isHttpOnly) \ + VALUES ( \ + :id, \ + :name, \ + :value, \ + :host, \ + :path, \ + :expiry, \ + :lastAccessed, \ + :isSecure, \ + :isHttpOnly)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies WHERE id = :id" + ); + + this.stmtUpdate = this.db.createStatement( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed WHERE id = :id" + ); + + break; + } + + case 3: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + baseDomain TEXT, \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + lastAccessed INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER)" + ); + + this.db.executeSimpleSQL( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)" + ); + } + + this.stmtInsert = this.db.createStatement( + "INSERT INTO moz_cookies ( \ + id, \ + baseDomain, \ + name, \ + value, \ + host, \ + path, \ + expiry, \ + lastAccessed, \ + isSecure, \ + isHttpOnly) \ + VALUES ( \ + :id, \ + :baseDomain, \ + :name, \ + :value, \ + :host, \ + :path, \ + :expiry, \ + :lastAccessed, \ + :isSecure, \ + :isHttpOnly)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies WHERE id = :id" + ); + + this.stmtUpdate = this.db.createStatement( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed WHERE id = :id" + ); + + break; + } + + case 4: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + baseDomain TEXT, \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + lastAccessed INTEGER, \ + creationTime INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER \ + CONSTRAINT moz_uniqueid UNIQUE (name, host, path))" + ); + + this.db.executeSimpleSQL( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)" + ); + + this.db.executeSimpleSQL("PRAGMA journal_mode = WAL"); + } + + this.stmtInsert = this.db.createStatement( + "INSERT INTO moz_cookies ( \ + baseDomain, \ + name, \ + value, \ + host, \ + path, \ + expiry, \ + lastAccessed, \ + creationTime, \ + isSecure, \ + isHttpOnly) \ + VALUES ( \ + :baseDomain, \ + :name, \ + :value, \ + :host, \ + :path, \ + :expiry, \ + :lastAccessed, \ + :creationTime, \ + :isSecure, \ + :isHttpOnly)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies \ + WHERE name = :name AND host = :host AND path = :path" + ); + + this.stmtUpdate = this.db.createStatement( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed \ + WHERE name = :name AND host = :host AND path = :path" + ); + + break; + } + + case 10: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + baseDomain TEXT, \ + originAttributes TEXT NOT NULL DEFAULT '', \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + lastAccessed INTEGER, \ + creationTime INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER, \ + inBrowserElement INTEGER DEFAULT 0, \ + sameSite INTEGER DEFAULT 0, \ + rawSameSite INTEGER DEFAULT 0, \ + CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))" + ); + + this.db.executeSimpleSQL( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)" + ); + + this.db.executeSimpleSQL("PRAGMA journal_mode = WAL"); + this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16"); + } + + this.stmtInsert = this.db.createStatement( + "INSERT INTO moz_cookies ( \ + name, \ + value, \ + host, \ + baseDomain, \ + path, \ + expiry, \ + lastAccessed, \ + creationTime, \ + isSecure, \ + isHttpOnly, \ + inBrowserElement, \ + originAttributes, \ + sameSite, \ + rawSameSite \ + ) VALUES ( \ + :name, \ + :value, \ + :host, \ + :baseDomain, \ + :path, \ + :expiry, \ + :lastAccessed, \ + :creationTime, \ + :isSecure, \ + :isHttpOnly, \ + :inBrowserElement, \ + :originAttributes, \ + :sameSite, \ + :rawSameSite)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies \ + WHERE name = :name AND host = :host AND path = :path AND \ + originAttributes = :originAttributes" + ); + + this.stmtUpdate = this.db.createStatement( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed \ + WHERE name = :name AND host = :host AND path = :path AND \ + originAttributes = :originAttributes" + ); + + break; + } + + case 11: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + originAttributes TEXT NOT NULL DEFAULT '', \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + lastAccessed INTEGER, \ + creationTime INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER, \ + inBrowserElement INTEGER DEFAULT 0, \ + sameSite INTEGER DEFAULT 0, \ + rawSameSite INTEGER DEFAULT 0, \ + CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))" + ); + + this.db.executeSimpleSQL("PRAGMA journal_mode = WAL"); + this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16"); + } + + this.stmtInsert = this.db.createStatement( + "INSERT INTO moz_cookies ( \ + name, \ + value, \ + host, \ + path, \ + expiry, \ + lastAccessed, \ + creationTime, \ + isSecure, \ + isHttpOnly, \ + inBrowserElement, \ + originAttributes, \ + sameSite, \ + rawSameSite \ + ) VALUES ( \ + :name, \ + :value, \ + :host, \ + :path, \ + :expiry, \ + :lastAccessed, \ + :creationTime, \ + :isSecure, \ + :isHttpOnly, \ + :inBrowserElement, \ + :originAttributes, \ + :sameSite, \ + :rawSameSite)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies \ + WHERE name = :name AND host = :host AND path = :path AND \ + originAttributes = :originAttributes" + ); + + this.stmtUpdate = this.db.createStatement( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed \ + WHERE name = :name AND host = :host AND path = :path AND \ + originAttributes = :originAttributes" + ); + + break; + } + + case 12: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + originAttributes TEXT NOT NULL DEFAULT '', \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + lastAccessed INTEGER, \ + creationTime INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER, \ + inBrowserElement INTEGER DEFAULT 0, \ + sameSite INTEGER DEFAULT 0, \ + rawSameSite INTEGER DEFAULT 0, \ + schemeMap INTEGER DEFAULT 0, \ + CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))" + ); + + this.db.executeSimpleSQL("PRAGMA journal_mode = WAL"); + this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16"); + } + + this.stmtInsert = this.db.createStatement( + "INSERT INTO moz_cookies ( \ + name, \ + value, \ + host, \ + path, \ + expiry, \ + lastAccessed, \ + creationTime, \ + isSecure, \ + isHttpOnly, \ + inBrowserElement, \ + originAttributes, \ + sameSite, \ + rawSameSite, \ + schemeMap \ + ) VALUES ( \ + :name, \ + :value, \ + :host, \ + :path, \ + :expiry, \ + :lastAccessed, \ + :creationTime, \ + :isSecure, \ + :isHttpOnly, \ + :inBrowserElement, \ + :originAttributes, \ + :sameSite, \ + :rawSameSite, \ + :schemeMap)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies \ + WHERE name = :name AND host = :host AND path = :path AND \ + originAttributes = :originAttributes" + ); + + this.stmtUpdate = this.db.createStatement( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed \ + WHERE name = :name AND host = :host AND path = :path AND \ + originAttributes = :originAttributes" + ); + + break; + } + + default: + do_throw("unrecognized schemaVersion!"); + } +} + +CookieDatabaseConnection.prototype = { + insertCookie(cookie) { + if (!(cookie instanceof Cookie)) { + do_throw("not a cookie"); + } + + switch (this.schema) { + case 1: + this.stmtInsert.bindByName("id", cookie.creationTime); + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + break; + + case 2: + this.stmtInsert.bindByName("id", cookie.creationTime); + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + break; + + case 3: + this.stmtInsert.bindByName("id", cookie.creationTime); + this.stmtInsert.bindByName("baseDomain", cookie.baseDomain); + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + break; + + case 4: + this.stmtInsert.bindByName("baseDomain", cookie.baseDomain); + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed); + this.stmtInsert.bindByName("creationTime", cookie.creationTime); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + break; + + case 10: + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("baseDomain", cookie.baseDomain); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed); + this.stmtInsert.bindByName("creationTime", cookie.creationTime); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement); + this.stmtInsert.bindByName( + "originAttributes", + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + this.stmtInsert.bindByName("sameSite", cookie.sameSite); + this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite); + break; + + case 11: + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed); + this.stmtInsert.bindByName("creationTime", cookie.creationTime); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement); + this.stmtInsert.bindByName( + "originAttributes", + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + this.stmtInsert.bindByName("sameSite", cookie.sameSite); + this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite); + break; + + case 12: + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed); + this.stmtInsert.bindByName("creationTime", cookie.creationTime); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement); + this.stmtInsert.bindByName( + "originAttributes", + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + this.stmtInsert.bindByName("sameSite", cookie.sameSite); + this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite); + this.stmtInsert.bindByName("schemeMap", cookie.schemeMap); + break; + + default: + do_throw("unrecognized schemaVersion!"); + } + + do_execute_stmt(this.stmtInsert); + }, + + deleteCookie(cookie) { + if (!(cookie instanceof Cookie)) { + do_throw("not a cookie"); + } + + switch (this.db.schemaVersion) { + case 1: + case 2: + case 3: + this.stmtDelete.bindByName("id", cookie.creationTime); + break; + + case 4: + this.stmtDelete.bindByName("name", cookie.name); + this.stmtDelete.bindByName("host", cookie.host); + this.stmtDelete.bindByName("path", cookie.path); + break; + + case 10: + case 11: + case 12: + this.stmtDelete.bindByName("name", cookie.name); + this.stmtDelete.bindByName("host", cookie.host); + this.stmtDelete.bindByName("path", cookie.path); + this.stmtDelete.bindByName( + "originAttributes", + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + break; + + default: + do_throw("unrecognized schemaVersion!"); + } + + do_execute_stmt(this.stmtDelete); + }, + + updateCookie(cookie) { + if (!(cookie instanceof Cookie)) { + do_throw("not a cookie"); + } + + switch (this.db.schemaVersion) { + case 1: + do_throw("can't update a schema 1 cookie!"); + break; + case 2: + case 3: + this.stmtUpdate.bindByName("id", cookie.creationTime); + this.stmtUpdate.bindByName("lastAccessed", cookie.lastAccessed); + break; + + case 4: + this.stmtDelete.bindByName("name", cookie.name); + this.stmtDelete.bindByName("host", cookie.host); + this.stmtDelete.bindByName("path", cookie.path); + this.stmtUpdate.bindByName("name", cookie.name); + this.stmtUpdate.bindByName("host", cookie.host); + this.stmtUpdate.bindByName("path", cookie.path); + this.stmtUpdate.bindByName("lastAccessed", cookie.lastAccessed); + break; + + case 10: + case 11: + case 12: + this.stmtDelete.bindByName("name", cookie.name); + this.stmtDelete.bindByName("host", cookie.host); + this.stmtDelete.bindByName("path", cookie.path); + this.stmtDelete.bindByName( + "originAttributes", + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + this.stmtUpdate.bindByName("name", cookie.name); + this.stmtUpdate.bindByName("host", cookie.host); + this.stmtUpdate.bindByName("path", cookie.path); + this.stmtUpdate.bindByName( + "originAttributes", + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + this.stmtUpdate.bindByName("lastAccessed", cookie.lastAccessed); + break; + + default: + do_throw("unrecognized schemaVersion!"); + } + + do_execute_stmt(this.stmtUpdate); + }, + + close() { + this.stmtInsert.finalize(); + this.stmtDelete.finalize(); + if (this.stmtUpdate) { + this.stmtUpdate.finalize(); + } + this.db.close(); + + this.stmtInsert = null; + this.stmtDelete = null; + this.stmtUpdate = null; + this.db = null; + }, +}; + +function do_get_cookie_file(profile) { + let file = profile.clone(); + file.append("cookies.sqlite"); + return file; +} + +// Count the cookies from 'host' in a database. If 'host' is null, count all +// cookies. +function do_count_cookies_in_db(connection, host) { + let select = null; + if (host) { + select = connection.createStatement( + "SELECT COUNT(1) FROM moz_cookies WHERE host = :host" + ); + select.bindByName("host", host); + } else { + select = connection.createStatement("SELECT COUNT(1) FROM moz_cookies"); + } + + select.executeStep(); + let result = select.getInt32(0); + select.reset(); + select.finalize(); + return result; +} + +// Execute 'stmt', ensuring that we reset it if it throws. +function do_execute_stmt(stmt) { + try { + stmt.executeStep(); + stmt.reset(); + } catch (e) { + stmt.reset(); + throw e; + } +} diff --git a/netwerk/test/unit/head_http3.js b/netwerk/test/unit/head_http3.js new file mode 100644 index 0000000000..56bc04db14 --- /dev/null +++ b/netwerk/test/unit/head_http3.js @@ -0,0 +1,105 @@ +/* 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-globals-from head_channels.js */ +/* import-globals-from head_cookies.js */ + +async function http3_setup_tests(http3version) { + let h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + let h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + `foo.example.com;${http3version}=:${h3Port}` + ); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + // `../unit/` so that unit_ipc tests can use as well + addCertFromFile(certdb, "../unit/http2-ca.pem", "CTu,u,u"); + + await setup_altsvc("https://foo.example.com/", h3Route, http3version); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let CheckHttp3Listener = function () {}; + +CheckHttp3Listener.prototype = { + expectedRoute: "", + http3version: "", + + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + if (routed == this.expectedRoute) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, this.http3version); + this.finish(true); + } else { + dump("try again to get alt svc mapping\n"); + this.finish(false); + } + }, +}; + +async function setup_altsvc(uri, expectedRoute, http3version) { + let result = false; + do { + let chan = makeChan(uri); + let listener = new CheckHttp3Listener(); + listener.expectedRoute = expectedRoute; + listener.http3version = http3version; + result = await altsvcSetupPromise(chan, listener); + dump("results=" + result); + } while (result === false); +} + +function altsvcSetupPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function http3_clear_prefs() { + Services.prefs.clearUserPref("network.http.http3.enable"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.dns.disableIPv6"); + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); + Services.prefs.clearUserPref("network.http.http3.support_version1"); + dump("cleanup done\n"); +} diff --git a/netwerk/test/unit/head_servers.js b/netwerk/test/unit/head_servers.js new file mode 100644 index 0000000000..e57e34d5c3 --- /dev/null +++ b/netwerk/test/unit/head_servers.js @@ -0,0 +1,889 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_trr.js */ + +/* globals require, __dirname, global, Buffer, process */ + +class BaseNodeHTTPServerCode { + static globalHandler(req, resp) { + let path = new URL(req.url, "http://example.com").pathname; + let handler = global.path_handlers[path]; + if (handler) { + return handler(req, resp); + } + + // Didn't find a handler for this path. + let response = `<h1> 404 Path not found: ${path}</h1>`; + resp.setHeader("Content-Type", "text/html"); + resp.setHeader("Content-Length", response.length); + resp.writeHead(404); + resp.end(response); + return undefined; + } +} + +class ADB { + static async stopForwarding(port) { + // return this.forwardPort(port, true); + } + + static async forwardPort(port, remove = false) { + if (!process.env.MOZ_ANDROID_DATA_DIR) { + // Not android, or we don't know how to do the forwarding + return; + } + // When creating a server on Android we must make sure that the port + // is forwarded from the host machine to the emulator. + let adb_path = "adb"; + if (process.env.MOZ_FETCHES_DIR) { + adb_path = `${process.env.MOZ_FETCHES_DIR}/android-sdk-linux/platform-tools/adb`; + } + + let command = `${adb_path} reverse tcp:${port} tcp:${port}`; + if (remove) { + command = `${adb_path} reverse --remove tcp:${port}`; + } + + await new Promise(resolve => { + const { exec } = require("child_process"); + exec(command, (error, stdout, stderr) => { + if (error) { + console.log(`error: ${error.message}`); + return; + } + if (stderr) { + console.log(`stderr: ${stderr}`); + } + // console.log(`stdout: ${stdout}`); + resolve(); + }); + }); + } +} + +class BaseNodeServer { + protocol() { + return this._protocol; + } + version() { + return this._version; + } + origin() { + return `${this.protocol()}://localhost:${this.port()}`; + } + port() { + return this._port; + } + domain() { + return `localhost`; + } + + /// Stops the server + async stop() { + if (this.processId) { + await this.execute(`ADB.stopForwarding(${this.port()})`); + await NodeServer.kill(this.processId); + this.processId = undefined; + } + } + + /// Executes a command in the context of the node server + async execute(command) { + return NodeServer.execute(this.processId, command); + } + + /// @path : string - the path on the server that we're handling. ex: /path + /// @handler : function(req, resp, url) - function that processes request and + /// emits a response. + async registerPathHandler(path, handler) { + return this.execute( + `global.path_handlers["${path}"] = ${handler.toString()}` + ); + } +} + +// HTTP + +class NodeHTTPServerCode extends BaseNodeHTTPServerCode { + static async startServer(port) { + const http = require("http"); + global.server = http.createServer(BaseNodeHTTPServerCode.globalHandler); + + await global.server.listen(port); + let serverPort = global.server.address().port; + await ADB.forwardPort(serverPort); + return serverPort; + } +} + +class NodeHTTPServer extends BaseNodeServer { + _protocol = "http"; + _version = "http/1.1"; + /// Starts the server + /// @port - default 0 + /// when provided, will attempt to listen on that port. + async start(port = 0) { + this.processId = await NodeServer.fork(); + + await this.execute(BaseNodeHTTPServerCode); + await this.execute(NodeHTTPServerCode); + await this.execute(ADB); + this._port = await this.execute(`NodeHTTPServerCode.startServer(${port})`); + await this.execute(`global.path_handlers = {};`); + } +} + +// HTTPS + +class NodeHTTPSServerCode extends BaseNodeHTTPServerCode { + static async startServer(port) { + const fs = require("fs"); + const options = { + key: fs.readFileSync(__dirname + "/http2-cert.key"), + cert: fs.readFileSync(__dirname + "/http2-cert.pem"), + }; + const https = require("https"); + global.server = https.createServer( + options, + BaseNodeHTTPServerCode.globalHandler + ); + + await global.server.listen(port); + let serverPort = global.server.address().port; + await ADB.forwardPort(serverPort); + return serverPort; + } +} + +class NodeHTTPSServer extends BaseNodeServer { + _protocol = "https"; + _version = "http/1.1"; + /// Starts the server + /// @port - default 0 + /// when provided, will attempt to listen on that port. + async start(port = 0) { + this.processId = await NodeServer.fork(); + + await this.execute(BaseNodeHTTPServerCode); + await this.execute(NodeHTTPSServerCode); + await this.execute(ADB); + this._port = await this.execute(`NodeHTTPSServerCode.startServer(${port})`); + await this.execute(`global.path_handlers = {};`); + } +} + +// HTTP2 + +class NodeHTTP2ServerCode extends BaseNodeHTTPServerCode { + static async startServer(port) { + const fs = require("fs"); + const options = { + key: fs.readFileSync(__dirname + "/http2-cert.key"), + cert: fs.readFileSync(__dirname + "/http2-cert.pem"), + }; + const http2 = require("http2"); + global.server = http2.createSecureServer( + options, + BaseNodeHTTPServerCode.globalHandler + ); + + await global.server.listen(port); + let serverPort = global.server.address().port; + await ADB.forwardPort(serverPort); + return serverPort; + } +} + +class NodeHTTP2Server extends BaseNodeServer { + _protocol = "https"; + _version = "h2"; + /// Starts the server + /// @port - default 0 + /// when provided, will attempt to listen on that port. + async start(port = 0) { + this.processId = await NodeServer.fork(); + + await this.execute(BaseNodeHTTPServerCode); + await this.execute(NodeHTTP2ServerCode); + await this.execute(ADB); + this._port = await this.execute(`NodeHTTP2ServerCode.startServer(${port})`); + await this.execute(`global.path_handlers = {};`); + } +} + +// Base HTTP proxy + +class BaseProxyCode { + static proxyHandler(req, res) { + if (req.url.startsWith("/")) { + res.writeHead(405); + res.end(); + return; + } + + let url = new URL(req.url); + const http = require("http"); + let preq = http + .request( + { + method: req.method, + path: url.pathname, + port: url.port, + host: url.hostname, + protocol: url.protocol, + }, + proxyresp => { + res.writeHead( + proxyresp.statusCode, + proxyresp.statusMessage, + proxyresp.headers + ); + proxyresp.on("data", chunk => { + if (!res.writableEnded) { + res.write(chunk); + } + }); + proxyresp.on("end", () => { + res.end(); + }); + } + ) + .on("error", e => { + console.log(`sock err: ${e}`); + }); + if (req.method != "POST") { + preq.end(); + } else { + req.on("data", chunk => { + if (!preq.writableEnded) { + preq.write(chunk); + } + }); + req.on("end", () => preq.end()); + } + } + + static onConnect(req, clientSocket, head) { + if (global.connect_handler) { + global.connect_handler(req, clientSocket, head); + return; + } + const net = require("net"); + // Connect to an origin server + const { port, hostname } = new URL(`https://${req.url}`); + const serverSocket = net + .connect(port || 443, hostname, () => { + clientSocket.write( + "HTTP/1.1 200 Connection Established\r\n" + + "Proxy-agent: Node.js-Proxy\r\n" + + "\r\n" + ); + serverSocket.write(head); + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + }) + .on("error", e => { + // The socket will error out when we kill the connection + // just ignore it. + }); + clientSocket.on("error", e => { + // Sometimes we got ECONNRESET error on windows platform. + // Ignore it for now. + }); + } +} + +class BaseHTTPProxy extends BaseNodeServer { + registerFilter() { + const pps = + Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + this.filter = new NodeProxyFilter( + this.protocol(), + "localhost", + this.port(), + 0 + ); + pps.registerFilter(this.filter, 10); + registerCleanupFunction(() => { + this.unregisterFilter(); + }); + } + + unregisterFilter() { + const pps = + Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + if (this.filter) { + pps.unregisterFilter(this.filter); + this.filter = undefined; + } + } + + /// Stops the server + async stop() { + this.unregisterFilter(); + await super.stop(); + } + + async registerConnectHandler(handler) { + return this.execute(`global.connect_handler = ${handler.toString()}`); + } +} + +// HTTP1 Proxy + +class NodeProxyFilter { + constructor(type, host, port, flags) { + this._type = type; + this._host = host; + this._port = port; + this._flags = flags; + this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]); + } + applyFilter(uri, pi, cb) { + const pps = + Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + cb.onProxyFilterResult( + pps.newProxyInfo( + this._type, + this._host, + this._port, + "", + "", + this._flags, + 1000, + null + ) + ); + } +} + +class HTTPProxyCode { + static async startServer(port) { + const http = require("http"); + global.proxy = http.createServer(BaseProxyCode.proxyHandler); + global.proxy.on("connect", BaseProxyCode.onConnect); + + await global.proxy.listen(port); + let proxyPort = global.proxy.address().port; + await ADB.forwardPort(proxyPort); + return proxyPort; + } +} + +class NodeHTTPProxyServer extends BaseHTTPProxy { + _protocol = "http"; + /// Starts the server + /// @port - default 0 + /// when provided, will attempt to listen on that port. + async start(port = 0) { + this.processId = await NodeServer.fork(); + + await this.execute(BaseProxyCode); + await this.execute(HTTPProxyCode); + await this.execute(ADB); + await this.execute(`global.connect_handler = null;`); + this._port = await this.execute(`HTTPProxyCode.startServer(${port})`); + + this.registerFilter(); + } +} + +// HTTPS proxy + +class HTTPSProxyCode { + static async startServer(port) { + const fs = require("fs"); + const options = { + key: fs.readFileSync(__dirname + "/proxy-cert.key"), + cert: fs.readFileSync(__dirname + "/proxy-cert.pem"), + }; + const https = require("https"); + global.proxy = https.createServer(options, BaseProxyCode.proxyHandler); + global.proxy.on("connect", BaseProxyCode.onConnect); + + await global.proxy.listen(port); + let proxyPort = global.proxy.address().port; + await ADB.forwardPort(proxyPort); + return proxyPort; + } +} + +class NodeHTTPSProxyServer extends BaseHTTPProxy { + _protocol = "https"; + /// Starts the server + /// @port - default 0 + /// when provided, will attempt to listen on that port. + async start(port = 0) { + this.processId = await NodeServer.fork(); + + await this.execute(BaseProxyCode); + await this.execute(HTTPSProxyCode); + await this.execute(ADB); + await this.execute(`global.connect_handler = null;`); + this._port = await this.execute(`HTTPSProxyCode.startServer(${port})`); + + this.registerFilter(); + } +} + +// HTTP2 proxy + +class HTTP2ProxyCode { + static async startServer(port, auth) { + const fs = require("fs"); + const options = { + key: fs.readFileSync(__dirname + "/proxy-cert.key"), + cert: fs.readFileSync(__dirname + "/proxy-cert.pem"), + }; + const http2 = require("http2"); + global.proxy = http2.createSecureServer(options); + global.socketCounts = {}; + this.setupProxy(auth); + + await global.proxy.listen(port); + let proxyPort = global.proxy.address().port; + await ADB.forwardPort(proxyPort); + return proxyPort; + } + + static setupProxy(auth) { + if (!global.proxy) { + throw new Error("proxy is null"); + } + + global.proxy.on("stream", (stream, headers) => { + if (headers[":scheme"] === "http") { + const http = require("http"); + let url = new URL( + `${headers[":scheme"]}://${headers[":authority"]}${headers[":path"]}` + ); + let req = http + .request( + { + method: headers[":method"], + path: headers[":path"], + port: url.port, + host: url.hostname, + protocol: url.protocol, + }, + proxyresp => { + let proxyheaders = Object.assign({}, proxyresp.headers); + // Filter out some prohibited headers. + ["connection", "transfer-encoding", "keep-alive"].forEach( + prop => { + delete proxyheaders[prop]; + } + ); + try { + stream.respond( + Object.assign( + { ":status": proxyresp.statusCode }, + proxyheaders + ) + ); + } catch (e) { + // The channel may have been closed already. + if (e.message != "The stream has been destroyed") { + throw e; + } + } + proxyresp.on("data", chunk => { + if (stream.writable) { + stream.write(chunk); + } + }); + proxyresp.on("end", () => { + stream.end(); + }); + } + ) + .on("error", e => { + console.log(`sock err: ${e}`); + }); + + if (headers[":method"] != "POST") { + req.end(); + } else { + stream.on("data", chunk => { + if (!req.writableEnded) { + req.write(chunk); + } + }); + stream.on("end", () => req.end()); + } + return; + } + if (headers[":method"] !== "CONNECT") { + // Only accept CONNECT requests + stream.respond({ ":status": 405 }); + stream.end(); + return; + } + + const authorization_token = headers["proxy-authorization"]; + if (auth && !authorization_token) { + stream.respond({ + ":status": 407, + "proxy-authenticate": "Basic realm='foo'", + }); + stream.end(); + return; + } + + const target = headers[":authority"]; + const { port } = new URL(`https://${target}`); + const net = require("net"); + const socket = net.connect(port, "127.0.0.1", () => { + try { + global.socketCounts[socket.remotePort] = + (global.socketCounts[socket.remotePort] || 0) + 1; + stream.respond({ ":status": 200 }); + socket.pipe(stream); + stream.pipe(socket); + } catch (exception) { + console.log(exception); + stream.close(); + } + }); + const http2 = require("http2"); + socket.on("error", error => { + const status = error.errno == "ENOTFOUND" ? 404 : 502; + try { + // If we already sent headers when the socket connected + // then sending the status again would throw. + if (!stream.sentHeaders) { + stream.respond({ ":status": status }); + } + stream.end(); + } catch (exception) { + stream.close(http2.constants.NGHTTP2_CONNECT_ERROR); + } + }); + stream.on("close", () => { + socket.end(); + }); + socket.on("close", () => { + stream.close(); + }); + stream.on("end", () => { + socket.end(); + }); + stream.on("aborted", () => { + socket.end(); + }); + stream.on("error", error => { + console.log("RESPONSE STREAM ERROR", error); + }); + }); + } + + static socketCount(port) { + return global.socketCounts[port]; + } +} + +class NodeHTTP2ProxyServer extends BaseHTTPProxy { + _protocol = "https"; + /// Starts the server + /// @port - default 0 + /// when provided, will attempt to listen on that port. + async start(port = 0, auth) { + this.processId = await NodeServer.fork(); + + await this.execute(BaseProxyCode); + await this.execute(HTTP2ProxyCode); + await this.execute(ADB); + await this.execute(`global.connect_handler = null;`); + this._port = await this.execute( + `HTTP2ProxyCode.startServer(${port}, ${auth})` + ); + + this.registerFilter(); + } + + async socketCount(port) { + let count = this.execute(`HTTP2ProxyCode.socketCount(${port})`); + return count; + } +} + +// websocket server + +class NodeWebSocketServerCode extends BaseNodeHTTPServerCode { + static messageHandler(data, ws) { + if (global.wsInputHandler) { + global.wsInputHandler(data, ws); + return; + } + + ws.send("test"); + } + + static async startServer(port) { + const fs = require("fs"); + const options = { + key: fs.readFileSync(__dirname + "/http2-cert.key"), + cert: fs.readFileSync(__dirname + "/http2-cert.pem"), + }; + const https = require("https"); + global.server = https.createServer( + options, + BaseNodeHTTPServerCode.globalHandler + ); + + let node_ws_root = `${__dirname}/../node-ws`; + const WebSocket = require(`${node_ws_root}/lib/websocket`); + WebSocket.Server = require(`${node_ws_root}/lib/websocket-server`); + global.webSocketServer = new WebSocket.Server({ server: global.server }); + global.webSocketServer.on("connection", function connection(ws) { + ws.on("message", data => + NodeWebSocketServerCode.messageHandler(data, ws) + ); + }); + + await global.server.listen(port); + let serverPort = global.server.address().port; + await ADB.forwardPort(serverPort); + + return serverPort; + } +} + +class NodeWebSocketServer extends BaseNodeServer { + _protocol = "wss"; + /// Starts the server + /// @port - default 0 + /// when provided, will attempt to listen on that port. + async start(port = 0) { + this.processId = await NodeServer.fork(); + + await this.execute(BaseNodeHTTPServerCode); + await this.execute(NodeWebSocketServerCode); + await this.execute(ADB); + this._port = await this.execute( + `NodeWebSocketServerCode.startServer(${port})` + ); + await this.execute(`global.path_handlers = {};`); + await this.execute(`global.wsInputHandler = null;`); + } + + async registerMessageHandler(handler) { + return this.execute(`global.wsInputHandler = ${handler.toString()}`); + } +} + +// websocket http2 server +// This code is inspired by +// https://github.com/szmarczak/http2-wrapper/blob/master/examples/ws/server.js +class NodeWebSocketHttp2ServerCode extends BaseNodeHTTPServerCode { + static async startServer(port) { + const fs = require("fs"); + const options = { + key: fs.readFileSync(__dirname + "/http2-cert.key"), + cert: fs.readFileSync(__dirname + "/http2-cert.pem"), + settings: { + enableConnectProtocol: true, + }, + }; + const http2 = require("http2"); + global.h2Server = http2.createSecureServer(options); + + let node_ws_root = `${__dirname}/../node-ws`; + const WebSocket = require(`${node_ws_root}/lib/websocket`); + + global.h2Server.on("stream", (stream, headers) => { + if (headers[":method"] === "CONNECT") { + stream.respond(); + + const ws = new WebSocket(null); + stream.setNoDelay = () => {}; + ws.setSocket(stream, Buffer.from(""), 100 * 1024 * 1024); + + ws.on("message", data => { + if (global.wsInputHandler) { + global.wsInputHandler(data, ws); + return; + } + + ws.send("test"); + }); + } else { + stream.respond(); + stream.end("ok"); + } + }); + + await global.h2Server.listen(port); + let serverPort = global.h2Server.address().port; + await ADB.forwardPort(serverPort); + + return serverPort; + } +} + +class NodeWebSocketHttp2Server extends BaseNodeServer { + _protocol = "h2ws"; + /// Starts the server + /// @port - default 0 + /// when provided, will attempt to listen on that port. + async start(port = 0) { + this.processId = await NodeServer.fork(); + + await this.execute(BaseNodeHTTPServerCode); + await this.execute(NodeWebSocketHttp2ServerCode); + await this.execute(ADB); + this._port = await this.execute( + `NodeWebSocketHttp2ServerCode.startServer(${port})` + ); + await this.execute(`global.path_handlers = {};`); + await this.execute(`global.wsInputHandler = null;`); + } + + async registerMessageHandler(handler) { + return this.execute(`global.wsInputHandler = ${handler.toString()}`); + } +} + +// Helper functions + +async function with_node_servers(arrayOfClasses, asyncClosure) { + for (let s of arrayOfClasses) { + let server = new s(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + await asyncClosure(server); + await server.stop(); + } +} + +// nsITLSServerSocket needs a certificate with a corresponding private key +// available. xpcshell tests can import the test file "client-cert.p12" using +// the password "password", resulting in a certificate with the common name +// "Test End-entity" being available with a corresponding private key. +function getTestServerCertificate() { + const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + const certFile = do_get_file("client-cert.p12"); + certDB.importPKCS12File(certFile, "password"); + for (const cert of certDB.getCerts()) { + if (cert.commonName == "Test End-entity") { + return cert; + } + } + return null; +} + +class WebSocketConnection { + constructor() { + this._openPromise = new Promise(resolve => { + this._openCallback = resolve; + }); + + this._stopPromise = new Promise(resolve => { + this._stopCallback = resolve; + }); + + this._msgPromise = new Promise(resolve => { + this._msgCallback = resolve; + }); + + this._proxyAvailablePromise = new Promise(resolve => { + this._proxyAvailCallback = resolve; + }); + + this._messages = []; + this._ws = null; + } + + get QueryInterface() { + return ChromeUtils.generateQI([ + "nsIWebSocketListener", + "nsIProtocolProxyCallback", + ]); + } + + onAcknowledge(aContext, aSize) {} + onBinaryMessageAvailable(aContext, aMsg) { + this._messages.push(aMsg); + this._msgCallback(); + } + onMessageAvailable(aContext, aMsg) {} + onServerClose(aContext, aCode, aReason) {} + onWebSocketListenerStart(aContext) {} + onStart(aContext) { + this._openCallback(); + } + onStop(aContext, aStatusCode) { + this._stopCallback({ status: aStatusCode }); + this._ws = null; + } + onProxyAvailable(req, chan, proxyInfo, status) { + if (proxyInfo) { + this._proxyAvailCallback({ type: proxyInfo.type }); + } else { + this._proxyAvailCallback({}); + } + } + + static makeWebSocketChan() { + let chan = Cc["@mozilla.org/network/protocol;1?name=wss"].createInstance( + Ci.nsIWebSocketChannel + ); + chan.initLoadInfo( + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_WEBSOCKET + ); + return chan; + } + // Returns a promise that resolves when the websocket channel is opened. + open(url) { + this._ws = WebSocketConnection.makeWebSocketChan(); + let uri = Services.io.newURI(url); + this._ws.asyncOpen(uri, url, {}, 0, this, null); + return this._openPromise; + } + // Closes the inner websocket. code and reason arguments are optional. + close(code, reason) { + this._ws.close(code || Ci.nsIWebSocketChannel.CLOSE_NORMAL, reason || ""); + } + // Sends a message to the server. + send(msg) { + this._ws.sendMsg(msg); + } + // Returns a promise that resolves when the channel's onStop is called. + // Promise resolves with an `{status}` object, where status is the + // result passed to onStop. + finished() { + return this._stopPromise; + } + getProxyInfo() { + return this._proxyAvailablePromise; + } + + // Returned promise resolves with an array of received messages + // If messages have been received in the the past before calling + // receiveMessages, the promise will immediately resolve. Otherwise + // it will resolve when the first message is received. + async receiveMessages() { + await this._msgPromise; + this._msgPromise = new Promise(resolve => { + this._msgCallback = resolve; + }); + let messages = this._messages; + this._messages = []; + return messages; + } +} diff --git a/netwerk/test/unit/head_telemetry.js b/netwerk/test/unit/head_telemetry.js new file mode 100644 index 0000000000..c3b1ec66aa --- /dev/null +++ b/netwerk/test/unit/head_telemetry.js @@ -0,0 +1,171 @@ +/* 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 { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +var HandshakeTelemetryHelpers = { + HISTOGRAMS: ["SSL_HANDSHAKE_RESULT", "SSL_TIME_UNTIL_READY"], + FLAVORS: ["", "_FIRST_TRY", "_CONSERVATIVE", "_ECH", "_ECH_GREASE"], + + /** + * Prints the Histogram to console. + * + * @param {*} name The identifier of the Histogram. + */ + dumpHistogram(name) { + let values = Services.telemetry.getHistogramById(name).snapshot().values; + dump(`${name}: ${JSON.stringify(values)}\n`); + }, + + /** + * Counts the number of entries in the histogram, ignoring the bucket value. + * e.g. {0: 1, 1: 2, 3: 3} has 6 entries. + * + * @param {Object} histObject The histogram to count the entries of. + * @returns The count of the number of entries in the histogram. + */ + countHistogramEntries(histObject) { + Assert.ok( + !mozinfo.socketprocess_networking, + "Histograms don't populate on network process" + ); + let count = 0; + let m = histObject.snapshot().values; + for (let k in m) { + count += m[k]; + } + return count; + }, + + /** + * Assert that the histogram index is the right value. It expects that + * other indexes are all zero. + * + * @param {Object} histogram The histogram to check. + * @param {Number} index The index to check against the expected value. + * @param {Number} expected The expected value of the index. + */ + assertHistogramMap(histogram, expectedEntries) { + Assert.ok( + !mozinfo.socketprocess_networking, + "Histograms don't populate on network process" + ); + let snapshot = JSON.parse(JSON.stringify(histogram)); + for (let [Tk, Tv] of expectedEntries.entries()) { + let found = false; + for (let [i, val] of Object.entries(snapshot.values)) { + if (i == Tk) { + found = true; + Assert.equal(val, Tv, `expected counts should match at index ${i}`); + snapshot.values[i] = 0; // Reset the value + } + } + Assert.ok(found, `Should have found an entry at index ${Tk}`); + } + for (let k in snapshot.values) { + Assert.equal( + snapshot.values[k], + 0, + `Should NOT have found an entry at index ${k} of value ${snapshot.values[k]}` + ); + } + }, + + /** + * Generates the pairwise concatonation of histograms and flavors. + * + * @param {Array} histogramList A subset of HISTOGRAMS. + * @param {Array} flavorList A subset of FLAVORS. + * @returns {Array} Valid TLS Histogram identifiers + */ + getHistogramNames(histogramList, flavorList) { + let output = []; + for (let h of histogramList) { + Assert.ok(this.HISTOGRAMS.includes(h), "Histogram name valid"); + for (let f of flavorList) { + Assert.ok(this.FLAVORS.includes(f), "Histogram flavor valid"); + output.push(h.concat(f)); + } + } + return output; + }, + + /** + * getHistogramNames but mapped to Histogram objects. + */ + getHistograms(histogramList, flavorList) { + return this.getHistogramNames(histogramList, flavorList).map(x => + Services.telemetry.getHistogramById(x) + ); + }, + + /** + * Clears TLS Handshake Histograms. + */ + resetHistograms() { + let allHistograms = this.getHistograms(this.HISTOGRAMS, this.FLAVORS); + for (let h of allHistograms) { + h.clear(); + } + }, + + /** + * Checks that all TLS Handshake Histograms of a particular flavor have + * exactly resultCount entries for the resultCode and no other entries. + * + * @param {Array} flavors An array of strings corresponding to which types + * of histograms should have entries. See + * HandshakeTelemetryHelpers.FLAVORS. + * @param {number} resultCode The expected result code, see sslerr.h. 0 is success, all others are errors. + * @param {number} resultCount The number of handshake results expected. + */ + checkEntry(flavors, resultCode, resultCount) { + Assert.ok( + !mozinfo.socketprocess_networking, + "Histograms don't populate on network process" + ); + // SSL_HANDSHAKE_RESULT_{FLAVOR} + for (let h of this.getHistograms(["SSL_HANDSHAKE_RESULT"], flavors)) { + TelemetryTestUtils.assertHistogram(h, resultCode, resultCount); + } + + // SSL_TIME_UNTIL_READY_{FLAVOR} should only contain values if we expected success. + if (resultCode === 0) { + for (let h of this.getHistograms(["SSL_TIME_UNTIL_READY"], flavors)) { + Assert.ok( + this.countHistogramEntries(h) === resultCount, + "Timing entry count correct" + ); + } + } else { + for (let h of this.getHistograms(["SSL_TIME_UNTIL_READY"], flavors)) { + Assert.ok( + this.countHistogramEntries(h) === 0, + "No timing entries expected" + ); + } + } + }, + + checkSuccess(flavors, resultCount = 1) { + this.checkEntry(flavors, 0, resultCount); + }, + + checkEmpty(flavors) { + for (let h of this.getHistogramNames(this.HISTOGRAMS, flavors)) { + let hObj = Services.telemetry.getHistogramById(h); + Assert.ok( + this.countHistogramEntries(hObj) === 0, + `No entries expected in ${h.name}. Contents: ${JSON.stringify( + hObj.snapshot() + )}` + ); + } + }, +}; diff --git a/netwerk/test/unit/head_trr.js b/netwerk/test/unit/head_trr.js new file mode 100644 index 0000000000..e83309f8bd --- /dev/null +++ b/netwerk/test/unit/head_trr.js @@ -0,0 +1,611 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ + +/* globals require, __dirname, global, Buffer, process */ + +const { NodeServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +/// Sets the TRR related prefs and adds the certificate we use for the HTTP2 +/// server. +function trr_test_setup() { + dump("start!\n"); + + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + + Services.prefs.setBoolPref("network.http.http2.enabled", true); + // the TRR server is on 127.0.0.1 + if (AppConstants.platform == "android") { + Services.prefs.setCharPref("network.trr.bootstrapAddr", "10.0.2.2"); + } else { + Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1"); + } + + // make all native resolve calls "secretly" resolve localhost instead + Services.prefs.setBoolPref("network.dns.native-is-localhost", true); + + Services.prefs.setBoolPref("network.trr.wait-for-portal", false); + // don't confirm that TRR is working, just go! + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + // some tests rely on the cache not being cleared on pref change. + // we specifically test that this works + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + // Turn off strict fallback mode and TRR retry for most tests, + // it is tested specifically. + Services.prefs.setBoolPref("network.trr.strict_native_fallback", false); + Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", false); + + // Turn off temp blocklist feature in tests. When enabled we may issue a + // lookup to resolve a parent name when blocklisting, which may bleed into + // and interfere with subsequent tasks. + Services.prefs.setBoolPref("network.trr.temp_blocklist", false); + + // We intentionally don't set the TRR mode. Each test should set it + // after setup in the first test. + + return h2Port; +} + +/// Clears the prefs that we're likely to set while testing TRR code +function trr_clear_prefs() { + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("network.trr.uri"); + Services.prefs.clearUserPref("network.trr.credentials"); + Services.prefs.clearUserPref("network.trr.wait-for-portal"); + Services.prefs.clearUserPref("network.trr.allow-rfc1918"); + Services.prefs.clearUserPref("network.trr.useGET"); + Services.prefs.clearUserPref("network.trr.confirmationNS"); + Services.prefs.clearUserPref("network.trr.bootstrapAddr"); + Services.prefs.clearUserPref("network.trr.temp_blocklist_duration_sec"); + Services.prefs.clearUserPref("network.trr.request_timeout_ms"); + Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms"); + Services.prefs.clearUserPref("network.trr.disable-ECS"); + Services.prefs.clearUserPref("network.trr.early-AAAA"); + Services.prefs.clearUserPref("network.trr.excluded-domains"); + Services.prefs.clearUserPref("network.trr.builtin-excluded-domains"); + Services.prefs.clearUserPref("network.trr.clear-cache-on-pref-change"); + Services.prefs.clearUserPref("network.trr.fetch_off_main_thread"); + Services.prefs.clearUserPref("captivedetect.canonicalURL"); + + Services.prefs.clearUserPref("network.http.http2.enabled"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.dns.native-is-localhost"); + Services.prefs.clearUserPref( + "network.trr.send_empty_accept-encoding_headers" + ); + Services.prefs.clearUserPref("network.trr.strict_native_fallback"); + Services.prefs.clearUserPref("network.trr.temp_blocklist"); +} + +/// This class sends a DNS query and can be awaited as a promise to get the +/// response. +class TRRDNSListener { + constructor(...args) { + if (args.length < 2) { + Assert.ok(false, "TRRDNSListener requires at least two arguments"); + } + this.name = args[0]; + if (typeof args[1] == "object") { + this.options = args[1]; + } else { + this.options = { + expectedAnswer: args[1], + expectedSuccess: args[2] ?? true, + delay: args[3], + trrServer: args[4] ?? "", + expectEarlyFail: args[5] ?? "", + flags: args[6] ?? 0, + type: args[7] ?? Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + port: args[8] ?? -1, + }; + } + this.expectedAnswer = this.options.expectedAnswer ?? undefined; + this.expectedSuccess = this.options.expectedSuccess ?? true; + this.delay = this.options.delay; + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + this.type = this.options.type ?? Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT; + let trrServer = this.options.trrServer || ""; + let port = this.options.port || -1; + + // This may be called in a child process that doesn't have Services available. + // eslint-disable-next-line mozilla/use-services + const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager + ); + const currentThread = threadManager.currentThread; + + this.additionalInfo = + trrServer == "" && port == -1 + ? null + : Services.dns.newAdditionalInfo(trrServer, port); + try { + this.request = Services.dns.asyncResolve( + this.name, + this.type, + this.options.flags || 0, + this.additionalInfo, + this, + currentThread, + {} // defaultOriginAttributes + ); + Assert.ok(!this.options.expectEarlyFail, "asyncResolve ok"); + } catch (e) { + Assert.ok(this.options.expectEarlyFail, "asyncResolve fail"); + this.resolve({ error: e }); + } + } + + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.ok( + inRequest == this.request, + "Checking that this is the correct callback" + ); + + // If we don't expect success here, just resolve and the caller will + // decide what to do with the results. + if (!this.expectedSuccess) { + this.resolve({ inRequest, inRecord, inStatus }); + return; + } + + Assert.equal(inStatus, Cr.NS_OK, "Checking status"); + + if (this.type != Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT) { + this.resolve({ inRequest, inRecord, inStatus }); + return; + } + + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + let answer = inRecord.getNextAddrAsString(); + Assert.equal( + answer, + this.expectedAnswer, + `Checking result for ${this.name}` + ); + inRecord.rewind(); // In case the caller also checks the addresses + + if (this.delay !== undefined) { + Assert.greaterOrEqual( + inRecord.trrFetchDurationNetworkOnly, + this.delay, + `the response should take at least ${this.delay}` + ); + + Assert.greaterOrEqual( + inRecord.trrFetchDuration, + this.delay, + `the response should take at least ${this.delay}` + ); + + if (this.delay == 0) { + // The response timing should be really 0 + Assert.equal( + inRecord.trrFetchDurationNetworkOnly, + 0, + `the response time should be 0` + ); + + Assert.equal( + inRecord.trrFetchDuration, + this.delay, + `the response time should be 0` + ); + } + } + + this.resolve({ inRequest, inRecord, inStatus }); + } + + QueryInterface(aIID) { + if (aIID.equals(Ci.nsIDNSListener) || aIID.equals(Ci.nsISupports)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } + + // Implement then so we can await this as a promise. + then() { + return this.promise.then.apply(this.promise, arguments); + } + + cancel(aStatus = Cr.NS_ERROR_ABORT) { + Services.dns.cancelAsyncResolve( + this.name, + this.type, + this.options.flags || 0, + this.resolverInfo, + this, + aStatus, + {} + ); + } +} + +/// Implements a basic HTTP2 server +class TRRServerCode { + static async startServer(port) { + const fs = require("fs"); + const options = { + key: fs.readFileSync(__dirname + "/http2-cert.key"), + cert: fs.readFileSync(__dirname + "/http2-cert.pem"), + }; + + const url = require("url"); + global.path_handlers = {}; + global.handler = (req, resp) => { + const path = req.headers[global.http2.constants.HTTP2_HEADER_PATH]; + let u = url.parse(req.url, true); + let handler = global.path_handlers[u.pathname]; + if (handler) { + handler(req, resp, u); + return; + } + + // Didn't find a handler for this path. + let response = `<h1> 404 Path not found: ${path}</h1>`; + resp.setHeader("Content-Type", "text/html"); + resp.setHeader("Content-Length", response.length); + resp.writeHead(404); + resp.end(response); + }; + + // key: string "name/type" + // value: array [answer1, answer2] + global.dns_query_answers = {}; + + // key: domain + // value: a map containing {key: type, value: number of requests} + global.dns_query_counts = {}; + + global.http2 = require("http2"); + global.server = global.http2.createSecureServer(options, global.handler); + + await global.server.listen(port); + + global.dnsPacket = require(`${__dirname}/../dns-packet`); + global.ip = require(`${__dirname}/../node_ip`); + + let serverPort = global.server.address().port; + + if (process.env.MOZ_ANDROID_DATA_DIR) { + // When creating a server on Android we must make sure that the port + // is forwarded from the host machine to the emulator. + let adb_path = "adb"; + if (process.env.MOZ_FETCHES_DIR) { + adb_path = `${process.env.MOZ_FETCHES_DIR}/android-sdk-linux/platform-tools/adb`; + } + + await new Promise(resolve => { + const { exec } = require("child_process"); + exec( + `${adb_path} reverse tcp:${serverPort} tcp:${serverPort}`, + (error, stdout, stderr) => { + if (error) { + console.log(`error: ${error.message}`); + return; + } + if (stderr) { + console.log(`stderr: ${stderr}`); + } + // console.log(`stdout: ${stdout}`); + resolve(); + } + ); + }); + } + + return serverPort; + } + + static getRequestCount(domain, type) { + if (!global.dns_query_counts[domain]) { + return 0; + } + return global.dns_query_counts[domain][type] || 0; + } +} + +/// This is the default handler for /dns-query +/// It implements basic functionality for parsing the DoH packet, then +/// queries global.dns_query_answers for available answers for the DNS query. +function trrQueryHandler(req, resp, url) { + let requestBody = Buffer.from(""); + let method = req.headers[global.http2.constants.HTTP2_HEADER_METHOD]; + let contentLength = req.headers["content-length"]; + + if (method == "POST") { + req.on("data", chunk => { + requestBody = Buffer.concat([requestBody, chunk]); + if (requestBody.length == contentLength) { + processRequest(req, resp, requestBody); + } + }); + } else if (method == "GET") { + if (!url.query.dns) { + resp.writeHead(400); + resp.end("Missing dns parameter"); + return; + } + + requestBody = Buffer.from(url.query.dns, "base64"); + processRequest(req, resp, requestBody); + } else { + // unexpected method. + resp.writeHead(405); + resp.end("Unexpected method"); + } + + function processRequest(req, resp, payload) { + let dnsQuery = global.dnsPacket.decode(payload); + let domain = dnsQuery.questions[0].name; + let type = dnsQuery.questions[0].type; + let response = global.dns_query_answers[`${domain}/${type}`] || {}; + + if (!global.dns_query_counts[domain]) { + global.dns_query_counts[domain] = {}; + } + global.dns_query_counts[domain][type] = + global.dns_query_counts[domain][type] + 1 || 1; + + let flags = global.dnsPacket.RECURSION_DESIRED; + if (!response.answers && !response.flags) { + flags |= 2; // SERVFAIL + } + flags |= response.flags || 0; + let buf = global.dnsPacket.encode({ + type: "response", + id: dnsQuery.id, + flags, + questions: dnsQuery.questions, + answers: response.answers || [], + additionals: response.additionals || [], + }); + + let writeResponse = (resp, buf, context) => { + try { + if (context.error) { + // If the error is a valid HTTP response number just write it out. + if (context.error < 600) { + resp.writeHead(context.error); + resp.end("Intentional error"); + return; + } + + // Bigger error means force close the session + req.stream.session.close(); + return; + } + resp.setHeader("Content-Length", buf.length); + resp.writeHead(200, { "Content-Type": "application/dns-message" }); + resp.write(buf); + resp.end(""); + } catch (e) {} + }; + + if (response.delay) { + // This function is handled within the httpserver where setTimeout is + // available. + // eslint-disable-next-line no-undef + setTimeout( + arg => { + writeResponse(arg[0], arg[1], arg[2]); + }, + response.delay, + [resp, buf, response] + ); + return; + } + + writeResponse(resp, buf, response); + } +} + +// A convenient wrapper around NodeServer +class TRRServer { + /// Starts the server + /// @port - default 0 + /// when provided, will attempt to listen on that port. + async start(port = 0) { + this.processId = await NodeServer.fork(); + + await this.execute(TRRServerCode); + this.port = await this.execute(`TRRServerCode.startServer(${port})`); + await this.registerPathHandler("/dns-query", trrQueryHandler); + } + + /// Executes a command in the context of the node server + async execute(command) { + return NodeServer.execute(this.processId, command); + } + + /// Stops the server + async stop() { + if (this.processId) { + await NodeServer.kill(this.processId); + this.processId = undefined; + } + } + + /// @path : string - the path on the server that we're handling. ex: /path + /// @handler : function(req, resp, url) - function that processes request and + /// emits a response. + async registerPathHandler(path, handler) { + return this.execute( + `global.path_handlers["${path}"] = ${handler.toString()}` + ); + } + + /// @name : string - name we're providing answers for. eg: foo.example.com + /// @type : string - the DNS query type. eg: "A", "AAAA", "CNAME", etc + /// @response : a map containing the response + /// answers: array of answers (hashmap) that dnsPacket can parse + /// eg: [{ + /// name: "bar.example.com", + /// ttl: 55, + /// type: "A", + /// flush: false, + /// data: "1.2.3.4", + /// }] + /// additionals - array of answers (hashmap) to be added to the additional section + /// delay: int - if not 0 the response will be sent with after `delay` ms. + /// flags: int - flags to be set on the answer + /// error: int - HTTP status. If truthy then the response will send this status + async registerDoHAnswers(name, type, response = {}) { + let text = `global.dns_query_answers["${name}/${type}"] = ${JSON.stringify( + response + )}`; + return this.execute(text); + } + + async requestCount(domain, type) { + return this.execute( + `TRRServerCode.getRequestCount("${domain}", "${type}")` + ); + } +} + +// Implements a basic HTTP2 proxy server +class TRRProxyCode { + static async startServer(endServerPort) { + const fs = require("fs"); + const options = { + key: fs.readFileSync(__dirname + "/http2-cert.key"), + cert: fs.readFileSync(__dirname + "/http2-cert.pem"), + }; + + const http2 = require("http2"); + global.proxy = http2.createSecureServer(options); + this.setupProxy(); + global.endServerPort = endServerPort; + + await global.proxy.listen(0); + + let serverPort = global.proxy.address().port; + return serverPort; + } + + static closeProxy() { + global.proxy.closeSockets(); + return new Promise(resolve => { + global.proxy.close(resolve); + }); + } + + static proxySessionCount() { + if (!global.proxy) { + return 0; + } + return global.proxy.proxy_session_count; + } + + static setupProxy() { + if (!global.proxy) { + throw new Error("proxy is null"); + } + global.proxy.proxy_session_count = 0; + global.proxy.on("session", () => { + ++global.proxy.proxy_session_count; + }); + + // We need to track active connections so we can forcefully close keep-alive + // connections when shutting down the proxy. + global.proxy.socketIndex = 0; + global.proxy.socketMap = {}; + global.proxy.on("connection", function (socket) { + let index = global.proxy.socketIndex++; + global.proxy.socketMap[index] = socket; + socket.on("close", function () { + delete global.proxy.socketMap[index]; + }); + }); + global.proxy.closeSockets = function () { + for (let i in global.proxy.socketMap) { + global.proxy.socketMap[i].destroy(); + } + }; + + global.proxy.on("stream", (stream, headers) => { + if (headers[":method"] !== "CONNECT") { + // Only accept CONNECT requests + stream.respond({ ":status": 405 }); + stream.end(); + return; + } + + const net = require("net"); + const socket = net.connect(global.endServerPort, "127.0.0.1", () => { + try { + stream.respond({ ":status": 200 }); + socket.pipe(stream); + stream.pipe(socket); + } catch (exception) { + console.log(exception); + stream.close(); + } + }); + socket.on("error", error => { + throw new Error( + `Unxpected error when conneting the HTTP/2 server from the HTTP/2 proxy during CONNECT handling: '${error}'` + ); + }); + }); + } +} + +class TRRProxy { + // Starts the proxy + async start(port) { + info("TRRProxy start!"); + this.processId = await NodeServer.fork(); + info("processid=" + this.processId); + await this.execute(TRRProxyCode); + this.port = await this.execute(`TRRProxyCode.startServer(${port})`); + Assert.notEqual(this.port, null); + this.initial_session_count = 0; + } + + // Executes a command in the context of the node server + async execute(command) { + return NodeServer.execute(this.processId, command); + } + + // Stops the server + async stop() { + if (this.processId) { + await NodeServer.execute(this.processId, `TRRProxyCode.closeProxy()`); + await NodeServer.kill(this.processId); + } + } + + async proxy_session_counter() { + let data = await NodeServer.execute( + this.processId, + `TRRProxyCode.proxySessionCount()` + ); + return parseInt(data) - this.initial_session_count; + } +} diff --git a/netwerk/test/unit/head_websocket.js b/netwerk/test/unit/head_websocket.js new file mode 100644 index 0000000000..84c5987f38 --- /dev/null +++ b/netwerk/test/unit/head_websocket.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/. */ + +"use strict"; + +function WebSocketListener(closure, ws, sentMsg) { + this._closure = closure; + this._ws = ws; + this._sentMsg = sentMsg; +} + +WebSocketListener.prototype = { + _closure: null, + _ws: null, + _sentMsg: null, + _received: null, + QueryInterface: ChromeUtils.generateQI(["nsIWebSocketListener"]), + + onAcknowledge(aContext, aSize) {}, + onBinaryMessageAvailable(aContext, aMsg) { + info("WsListener::onBinaryMessageAvailable"); + this._received = aMsg; + this._ws.close(0, null); + }, + onMessageAvailable(aContext, aMsg) {}, + onServerClose(aContext, aCode, aReason) {}, + onWebSocketListenerStart(aContext) {}, + onStart(aContext) { + this._ws.sendMsg(this._sentMsg); + }, + onStop(aContext, aStatusCode) { + try { + this._closure(aStatusCode, this._received); + this._ws = null; + } catch (ex) { + do_throw("Error in closure function: " + ex); + } + }, +}; + +function makeWebSocketChan() { + let chan = Cc["@mozilla.org/network/protocol;1?name=wss"].createInstance( + Ci.nsIWebSocketChannel + ); + chan.initLoadInfo( + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_WEBSOCKET + ); + return chan; +} + +function openWebSocketChannelPromise(chan, url, msg) { + let uri = Services.io.newURI(url); + return new Promise(resolve => { + function finish(status, result) { + resolve([status, result]); + } + chan.asyncOpen( + uri, + url, + {}, + 0, + new WebSocketListener(finish, chan, msg), + null + ); + }); +} diff --git a/netwerk/test/unit/head_webtransport.js b/netwerk/test/unit/head_webtransport.js new file mode 100644 index 0000000000..99432e950d --- /dev/null +++ b/netwerk/test/unit/head_webtransport.js @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from head_cookies.js */ + +let WebTransportListener = function () {}; + +WebTransportListener.prototype = { + onSessionReady(sessionId) { + info("SessionId " + sessionId); + this.ready(); + }, + onSessionClosed(errorCode, reason) { + info("Error: " + errorCode + " reason: " + reason); + if (this.closed) { + this.closed(); + } + }, + onIncomingBidirectionalStreamAvailable(stream) { + info("got incoming bidirectional stream"); + this.streamAvailable(stream); + }, + onIncomingUnidirectionalStreamAvailable(stream) { + info("got incoming unidirectional stream"); + this.streamAvailable(stream); + }, + onDatagramReceived(data) { + info("got datagram"); + if (this.onDatagram) { + this.onDatagram(data); + } + }, + onMaxDatagramSize(size) { + info("max datagram size: " + size); + if (this.onMaxDatagramSize) { + this.onMaxDatagramSize(size); + } + }, + onOutgoingDatagramOutCome(id, outcome) { + if (this.onDatagramOutcome) { + this.onDatagramOutcome({ id, outcome }); + } + }, + + QueryInterface: ChromeUtils.generateQI(["WebTransportSessionEventListener"]), +}; + +function WebTransportStreamCallback() {} + +WebTransportStreamCallback.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIWebTransportStreamCallback"]), + + onBidirectionalStreamReady(aStream) { + Assert.ok(aStream != null); + this.finish(aStream); + }, + onUnidirectionalStreamReady(aStream) { + Assert.ok(aStream != null); + this.finish(aStream); + }, + onError(aError) { + this.finish(aError); + }, +}; + +function StreamStatsCallback() {} + +StreamStatsCallback.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebTransportStreamStatsCallback", + ]), + + onSendStatsAvailable(aStats) { + Assert.ok(aStats != null); + this.finish(aStats); + }, + onReceiveStatsAvailable(aStats) { + Assert.ok(aStats != null); + this.finish(aStats); + }, +}; + +function inputStreamReader() {} + +inputStreamReader.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInputStreamCallback"]), + + onInputStreamReady(input) { + let data = NetUtil.readInputStreamToString(input, input.available()); + this.finish(data); + }, +}; + +function streamCreatePromise(transport, bidi) { + return new Promise(resolve => { + let listener = new WebTransportStreamCallback().QueryInterface( + Ci.nsIWebTransportStreamCallback + ); + listener.finish = resolve; + + if (bidi) { + transport.createOutgoingBidirectionalStream(listener); + } else { + transport.createOutgoingUnidirectionalStream(listener); + } + }); +} + +function sendStreamStatsPromise(stream) { + return new Promise(resolve => { + let listener = new StreamStatsCallback().QueryInterface( + Ci.nsIWebTransportStreamStatsCallback + ); + listener.finish = resolve; + + stream.QueryInterface(Ci.nsIWebTransportSendStream); + stream.getSendStreamStats(listener); + }); +} + +function receiveStreamStatsPromise(stream) { + return new Promise(resolve => { + let listener = new StreamStatsCallback().QueryInterface( + Ci.nsIWebTransportStreamStatsCallback + ); + listener.finish = resolve; + + stream.QueryInterface(Ci.nsIWebTransportReceiveStream); + stream.getReceiveStreamStats(listener); + }); +} diff --git a/netwerk/test/unit/http2-ca.pem b/netwerk/test/unit/http2-ca.pem new file mode 100644 index 0000000000..ef5a801720 --- /dev/null +++ b/netwerk/test/unit/http2-ca.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC1DCCAbygAwIBAgIURZvN7yVqFNwThGHASoy1OlOGvOMwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOIEhUVFAyIFRlc3QgQ0EwIhgPMjAxNzAxMDEwMDAwMDBa +GA8yMDI3MDEwMTAwMDAwMFowGTEXMBUGA1UEAwwOIEhUVFAyIFRlc3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6iFGoRI4W1kH9braIBjYQPTwT +2erkNUq07PVoV2wke8HHJajg2B+9sZwGm24ahvJr4q9adWtqZHEIeqVap0WH9xzV +JJwCfs1D/B5p0DggKZOrIMNJ5Nu5TMJrbA7tFYIP8X6taRqx0wI6iypB7qdw4A8N +jf1mCyuwJJKkfbmIYXmQsVeQPdI7xeC4SB+oN9OIQ+8nFthVt2Zaqn4CkC86exCA +BiTMHGyXrZZhW7filhLAdTGjDJHdtMr3/K0dJdMJ77kXDqdo4bN7LyJvaeO0ipVh +He4m1iWdq5EITjbLHCQELL8Wiy/l8Y+ZFzG4s/5JI/pyUcQx1QOs2hgKNe2NAgMB +AAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADyDiQnKjsvR +NrOk0aqgJ8XgK/IgJXFLbAVivjBLwnJGEkwxrFtC14mpTrPuXw9AybhroMjinq4Y +cNYTFuTE34k0fZEU8d60J/Tpfd1i0EB8+oUPuqOn+N29/LeHPAnkDJdOZye3w0U+ +StAI79WqUYQaKIG7qLnt60dQwBte12uvbuPaB3mREIfDXOKcjLBdZHL1waWjtzUX +z2E91VIdpvJGfEfXC3fIe1uO9Jh/E9NVWci84+njkNsl+OyBfOJ8T+pV3SHfWedp +Zbjwh6UTukIuc3mW0rS/qZOa2w3HQaO53BMbluo0w1+cscOepsATld2HHvSiHB+0 +K8SWFRHdBOU= +-----END CERTIFICATE----- diff --git a/netwerk/test/unit/http2-ca.pem.certspec b/netwerk/test/unit/http2-ca.pem.certspec new file mode 100644 index 0000000000..46f62e3fbc --- /dev/null +++ b/netwerk/test/unit/http2-ca.pem.certspec @@ -0,0 +1,4 @@ +issuer: HTTP2 Test CA +subject: HTTP2 Test CA +validity:20170101-20270101 +extension:basicConstraints:cA, diff --git a/netwerk/test/unit/http2_test_common.js b/netwerk/test/unit/http2_test_common.js new file mode 100644 index 0000000000..341aa191da --- /dev/null +++ b/netwerk/test/unit/http2_test_common.js @@ -0,0 +1,1504 @@ +// test HTTP/2 + +"use strict"; + +/* import-globals-from head_channels.js */ + +// Generate a small and a large post with known pre-calculated md5 sums +function generateContent(size) { + var content = ""; + for (var i = 0; i < size; i++) { + content += "0"; + } + return content; +} + +var posts = []; +posts.push(generateContent(10)); +posts.push(generateContent(250000)); +posts.push(generateContent(128000)); + +// pre-calculated md5sums (in hex) of the above posts +var md5s = [ + "f1b708bba17f1ce948dc979f4d7092bc", + "2ef8d3b6c8f329318eb1a119b12622b6", +]; + +var bigListenerData = generateContent(128 * 1024); +var bigListenerMD5 = "8f607cfdd2c87d6a7eedb657dafbd836"; + +function checkIsHttp2(request) { + try { + if (request.getResponseHeader("X-Firefox-Spdy") == "h2") { + if (request.getResponseHeader("X-Connection-Http2") == "yes") { + return true; + } + return false; // Weird case, but the server disagrees with us + } + } catch (e) { + // Nothing to do here + } + return false; +} + +var Http2CheckListener = function () {}; + +Http2CheckListener.prototype = { + onStartRequestFired: false, + onDataAvailableFired: false, + isHttp2Connection: false, + shouldBeHttp2: true, + accum: 0, + expected: -1, + shouldSucceed: true, + + onStartRequest: function testOnStartRequest(request) { + this.onStartRequestFired = true; + if (this.shouldSucceed && !Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code! (" + request.status + ")"); + } else if ( + !this.shouldSucceed && + Components.isSuccessCode(request.status) + ) { + do_throw("Channel succeeded unexpectedly!"); + } + + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.requestSucceeded, this.shouldSucceed); + if (this.shouldSucceed) { + Assert.equal(request.responseStatus, 200); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.accum += cnt; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.ok(this.onStartRequestFired); + if (this.expected != -1) { + Assert.equal(this.accum, this.expected); + } + + if (this.shouldSucceed) { + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection == this.shouldBeHttp2); + } else { + Assert.ok(!Components.isSuccessCode(status)); + } + request.QueryInterface(Ci.nsIProxiedChannel); + var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + this.finish({ httpProxyConnectResponseCode }); + }, +}; + +/* + * Support for testing valid multiplexing of streams + */ + +var multiplexContent = generateContent(30 * 1024); + +/* Listener class to control the testing of multiplexing */ +var Http2MultiplexListener = function () {}; + +Http2MultiplexListener.prototype = new Http2CheckListener(); + +Http2MultiplexListener.prototype.streamID = 0; +Http2MultiplexListener.prototype.buffer = ""; + +Http2MultiplexListener.prototype.onDataAvailable = function ( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.streamID = parseInt(request.getResponseHeader("X-Http2-StreamID")); + var data = read_stream(stream, cnt); + this.buffer = this.buffer.concat(data); +}; + +Http2MultiplexListener.prototype.onStopRequest = function (request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection); + Assert.ok(this.buffer == multiplexContent); + + request.QueryInterface(Ci.nsIProxiedChannel); + // This is what does most of the hard work for us + var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + var streamID = this.streamID; + this.finish({ httpProxyConnectResponseCode, streamID }); +}; + +// Does the appropriate checks for header gatewaying +var Http2HeaderListener = function (name, callback) { + this.name = name; + this.callback = callback; +}; + +Http2HeaderListener.prototype = new Http2CheckListener(); +Http2HeaderListener.prototype.value = ""; + +Http2HeaderListener.prototype.onDataAvailable = function ( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + var hvalue = request.getResponseHeader(this.name); + Assert.notEqual(hvalue, ""); + this.callback(hvalue); + read_stream(stream, cnt); +}; + +var Http2PushListener = function (shouldBePushed) { + this.shouldBePushed = shouldBePushed; +}; + +Http2PushListener.prototype = new Http2CheckListener(); + +Http2PushListener.prototype.onDataAvailable = function ( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + if ( + request.originalURI.spec == + `https://localhost:${this.serverPort}/push.js` || + request.originalURI.spec == + `https://localhost:${this.serverPort}/push2.js` || + request.originalURI.spec == `https://localhost:${this.serverPort}/push5.js` + ) { + Assert.equal( + request.getResponseHeader("pushed"), + this.shouldBePushed ? "yes" : "no" + ); + } + read_stream(stream, cnt); +}; + +const pushHdrTxt = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +const pullHdrTxt = pushHdrTxt.split("").reverse().join(""); + +function checkContinuedHeaders(getHeader, headerPrefix, headerText) { + for (var i = 0; i < 265; i++) { + Assert.equal(getHeader(headerPrefix + 1), headerText); + } +} + +var Http2ContinuedHeaderListener = function () {}; + +Http2ContinuedHeaderListener.prototype = new Http2CheckListener(); + +Http2ContinuedHeaderListener.prototype.onStopsLeft = 2; + +Http2ContinuedHeaderListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIHttpPushListener", + "nsIStreamListener", +]); + +Http2ContinuedHeaderListener.prototype.getInterface = function (aIID) { + return this.QueryInterface(aIID); +}; + +Http2ContinuedHeaderListener.prototype.onDataAvailable = function ( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + if ( + request.originalURI.spec == + `https://localhost:${this.serverPort}/continuedheaders` + ) { + // This is the original request, so the only one where we'll have continued response headers + checkContinuedHeaders( + request.getResponseHeader, + "X-Pull-Test-Header-", + pullHdrTxt + ); + } + read_stream(stream, cnt); +}; + +Http2ContinuedHeaderListener.prototype.onStopRequest = function ( + request, + status +) { + Assert.ok(this.onStartRequestFired); + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection); + + --this.onStopsLeft; + if (this.onStopsLeft === 0) { + request.QueryInterface(Ci.nsIProxiedChannel); + var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + this.finish({ httpProxyConnectResponseCode }); + } +}; + +Http2ContinuedHeaderListener.prototype.onPush = function ( + associatedChannel, + pushChannel +) { + Assert.equal( + associatedChannel.originalURI.spec, + "https://localhost:" + this.serverPort + "/continuedheaders" + ); + Assert.equal(pushChannel.getRequestHeader("x-pushed-request"), "true"); + checkContinuedHeaders( + pushChannel.getRequestHeader, + "X-Push-Test-Header-", + pushHdrTxt + ); + + pushChannel.asyncOpen(this); +}; + +// Does the appropriate checks for a large GET response +var Http2BigListener = function () {}; + +Http2BigListener.prototype = new Http2CheckListener(); +Http2BigListener.prototype.buffer = ""; + +Http2BigListener.prototype.onDataAvailable = function ( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.buffer = this.buffer.concat(read_stream(stream, cnt)); + // We know the server should send us the same data as our big post will be, + // so the md5 should be the same + Assert.equal(bigListenerMD5, request.getResponseHeader("X-Expected-MD5")); +}; + +Http2BigListener.prototype.onStopRequest = function (request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection); + + // Don't want to flood output, so don't use do_check_eq + Assert.ok(this.buffer == bigListenerData); + + request.QueryInterface(Ci.nsIProxiedChannel); + var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + this.finish({ httpProxyConnectResponseCode }); +}; + +var Http2HugeSuspendedListener = function () {}; + +Http2HugeSuspendedListener.prototype = new Http2CheckListener(); +Http2HugeSuspendedListener.prototype.count = 0; + +Http2HugeSuspendedListener.prototype.onDataAvailable = function ( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.count += cnt; + read_stream(stream, cnt); +}; + +Http2HugeSuspendedListener.prototype.onStopRequest = function ( + request, + status +) { + Assert.ok(this.onStartRequestFired); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection); + Assert.equal(this.count, 1024 * 1024 * 1); // 1mb of data expected + request.QueryInterface(Ci.nsIProxiedChannel); + var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + this.finish({ httpProxyConnectResponseCode }); +}; + +// Does the appropriate checks for POSTs +var Http2PostListener = function (expected_md5) { + this.expected_md5 = expected_md5; +}; + +Http2PostListener.prototype = new Http2CheckListener(); +Http2PostListener.prototype.expected_md5 = ""; + +Http2PostListener.prototype.onDataAvailable = function ( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + read_stream(stream, cnt); + Assert.equal( + this.expected_md5, + request.getResponseHeader("X-Calculated-MD5") + ); +}; + +var ResumeStalledChannelListener = function () {}; + +ResumeStalledChannelListener.prototype = { + onStartRequestFired: false, + onDataAvailableFired: false, + isHttp2Connection: false, + shouldBeHttp2: true, + resumable: null, + + onStartRequest: function testOnStartRequest(request) { + this.onStartRequestFired = true; + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code! (" + request.status + ")"); + } + + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.responseStatus, 200); + Assert.equal(request.requestSucceeded, true); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection == this.shouldBeHttp2); + this.resumable.resume(); + }, +}; + +// test a large download that creates stream flow control and +// confirm we can do another independent stream while the download +// stream is stuck +async function test_http2_blocking_download(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/bigdownload`); + var internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.initialRwin = 500000; // make the stream.suspend push back in h2 + var p = new Promise(resolve => { + var listener = new Http2CheckListener(); + listener.finish = resolve; + listener.expected = 3 * 1024 * 1024; + chan.asyncOpen(listener); + chan.suspend(); + }); + // wait 5 seconds so that stream flow control kicks in and then see if we + // can do a basic transaction (i.e. session not blocked). afterwards resume + // channel + do_timeout(5000, function () { + var simpleChannel = makeHTTPChannel(`https://localhost:${serverPort}/`); + var sl = new ResumeStalledChannelListener(); + sl.resumable = chan; + simpleChannel.asyncOpen(sl); + }); + return p; +} + +// Make sure we make a HTTP2 connection and both us and the server mark it as such +async function test_http2_basic(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/`); + var p = new Promise(resolve => { + var listener = new Http2CheckListener(); + listener.finish = resolve; + chan.asyncOpen(listener); + }); + return p; +} + +async function test_http2_basic_unblocked_dep(serverPort) { + var chan = makeHTTPChannel( + `https://localhost:${serverPort}/basic_unblocked_dep` + ); + var cos = chan.QueryInterface(Ci.nsIClassOfService); + cos.addClassFlags(Ci.nsIClassOfService.Unblocked); + return new Promise(resolve => { + var listener = new Http2CheckListener(); + listener.finish = resolve; + chan.asyncOpen(listener); + }); +} + +// make sure we don't use h2 when disallowed +async function test_http2_nospdy(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/`); + return new Promise(resolve => { + var listener = new Http2CheckListener(); + listener.finish = resolve; + var internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.allowSpdy = false; + listener.shouldBeHttp2 = false; + chan.asyncOpen(listener); + }); +} + +// Support for making sure XHR works over SPDY +function checkXhr(xhr, finish) { + if (xhr.readyState != 4) { + return; + } + + Assert.equal(xhr.status, 200); + Assert.equal(checkIsHttp2(xhr), true); + finish(); +} + +// Fires off an XHR request over h2 +async function test_http2_xhr(serverPort) { + return new Promise(resolve => { + var req = new XMLHttpRequest(); + req.open("GET", `https://localhost:${serverPort}/`, true); + req.addEventListener("readystatechange", function (evt) { + checkXhr(req, resolve); + }); + req.send(null); + }); +} + +var Http2ConcurrentListener = function () {}; + +Http2ConcurrentListener.prototype = new Http2CheckListener(); +Http2ConcurrentListener.prototype.count = 0; +Http2ConcurrentListener.prototype.target = 0; +Http2ConcurrentListener.prototype.reset = 0; +Http2ConcurrentListener.prototype.recvdHdr = 0; + +Http2ConcurrentListener.prototype.onStopRequest = function (request, status) { + this.count++; + Assert.ok(this.isHttp2Connection); + if (this.recvdHdr > 0) { + Assert.equal(request.getResponseHeader("X-Recvd"), this.recvdHdr); + } + + if (this.count == this.target) { + if (this.reset > 0) { + Services.prefs.setIntPref( + "network.http.http2.default-concurrent", + this.reset + ); + } + request.QueryInterface(Ci.nsIProxiedChannel); + var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + this.finish({ httpProxyConnectResponseCode }); + } +}; + +async function test_http2_concurrent(concurrent_channels, serverPort) { + var p = new Promise(resolve => { + var concurrent_listener = new Http2ConcurrentListener(); + concurrent_listener.finish = resolve; + concurrent_listener.target = 201; + concurrent_listener.reset = Services.prefs.getIntPref( + "network.http.http2.default-concurrent" + ); + Services.prefs.setIntPref("network.http.http2.default-concurrent", 100); + + for (var i = 0; i < concurrent_listener.target; i++) { + concurrent_channels[i] = makeHTTPChannel( + `https://localhost:${serverPort}/750ms` + ); + concurrent_channels[i].loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + concurrent_channels[i].asyncOpen(concurrent_listener); + } + }); + return p; +} + +async function test_http2_concurrent_post(concurrent_channels, serverPort) { + return new Promise(resolve => { + var concurrent_listener = new Http2ConcurrentListener(); + concurrent_listener.finish = resolve; + concurrent_listener.target = 8; + concurrent_listener.recvdHdr = posts[2].length; + concurrent_listener.reset = Services.prefs.getIntPref( + "network.http.http2.default-concurrent" + ); + Services.prefs.setIntPref("network.http.http2.default-concurrent", 3); + + for (var i = 0; i < concurrent_listener.target; i++) { + concurrent_channels[i] = makeHTTPChannel( + `https://localhost:${serverPort}/750msPost` + ); + concurrent_channels[i].loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = posts[2]; + var uchan = concurrent_channels[i].QueryInterface(Ci.nsIUploadChannel); + uchan.setUploadStream(stream, "text/plain", stream.available()); + concurrent_channels[i].requestMethod = "POST"; + concurrent_channels[i].asyncOpen(concurrent_listener); + } + }); +} + +// Test to make sure we get multiplexing right +async function test_http2_multiplex(serverPort) { + let chan1 = makeHTTPChannel(`https://localhost:${serverPort}/multiplex1`); + let chan2 = makeHTTPChannel(`https://localhost:${serverPort}/multiplex2`); + let listener1 = new Http2MultiplexListener(); + let listener2 = new Http2MultiplexListener(); + + let promises = []; + let p1 = new Promise(resolve => { + listener1.finish = resolve; + }); + promises.push(p1); + let p2 = new Promise(resolve => { + listener2.finish = resolve; + }); + promises.push(p2); + + chan1.asyncOpen(listener1); + chan2.asyncOpen(listener2); + return Promise.all(promises); +} + +// Test to make sure we gateway non-standard headers properly +async function test_http2_header(serverPort) { + let chan = makeHTTPChannel(`https://localhost:${serverPort}/header`); + let hvalue = "Headers are fun"; + chan.setRequestHeader("X-Test-Header", hvalue, false); + return new Promise(resolve => { + let listener = new Http2HeaderListener("X-Received-Test-Header", function ( + received_hvalue + ) { + Assert.equal(received_hvalue, hvalue); + }); + listener.finish = resolve; + chan.asyncOpen(listener); + }); +} + +// Test to make sure headers with invalid characters in the name are rejected +async function test_http2_invalid_response_header(serverPort, invalid_kind) { + return new Promise(resolve => { + var listener = new Http2CheckListener(); + listener.finish = resolve; + listener.shouldSucceed = false; + var chan = makeHTTPChannel( + `https://localhost:${serverPort}/invalid_response_header/${invalid_kind}` + ); + chan.asyncOpen(listener); + }); +} + +// Test to make sure cookies are split into separate fields before compression +async function test_http2_cookie_crumbling(serverPort) { + var chan = makeHTTPChannel( + `https://localhost:${serverPort}/cookie_crumbling` + ); + var cookiesSent = ["a=b", "c=d01234567890123456789", "e=f"].sort(); + chan.setRequestHeader("Cookie", cookiesSent.join("; "), false); + return new Promise(resolve => { + var listener = new Http2HeaderListener("X-Received-Header-Pairs", function ( + pairsReceived + ) { + var cookiesReceived = JSON.parse(pairsReceived) + .filter(function (pair) { + return pair[0] == "cookie"; + }) + .map(function (pair) { + return pair[1]; + }) + .sort(); + Assert.equal(cookiesReceived.length, cookiesSent.length); + cookiesReceived.forEach(function (cookieReceived, index) { + Assert.equal(cookiesSent[index], cookieReceived); + }); + }); + listener.finish = resolve; + chan.asyncOpen(listener); + }); +} + +async function test_http2_push1(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/push`); + chan.loadGroup = loadGroup; + return new Promise(resolve => { + var listener = new Http2PushListener(true); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.asyncOpen(listener); + }); +} + +async function test_http2_push2(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/push.js`); + chan.loadGroup = loadGroup; + return new Promise(resolve => { + var listener = new Http2PushListener(true); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.asyncOpen(listener); + }); +} + +async function test_http2_push3(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/push2`); + chan.loadGroup = loadGroup; + return new Promise(resolve => { + var listener = new Http2PushListener(true); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.asyncOpen(listener); + }); +} + +async function test_http2_push4(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/push2.js`); + chan.loadGroup = loadGroup; + return new Promise(resolve => { + var listener = new Http2PushListener(true); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.asyncOpen(listener); + }); +} + +async function test_http2_push5(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/push5`); + chan.loadGroup = loadGroup; + return new Promise(resolve => { + var listener = new Http2PushListener(true); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.asyncOpen(listener); + }); +} + +async function test_http2_push6(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/push5.js`); + chan.loadGroup = loadGroup; + return new Promise(resolve => { + var listener = new Http2PushListener(true); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.asyncOpen(listener); + }); +} + +// this is a basic test where the server sends a simple document with 2 header +// blocks. bug 1027364 +async function test_http2_doubleheader(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/doubleheader`); + return new Promise(resolve => { + var listener = new Http2CheckListener(); + listener.finish = resolve; + chan.asyncOpen(listener); + }); +} + +// Make sure we handle GETs that cover more than 2 frames properly +async function test_http2_big(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/big`); + return new Promise(resolve => { + var listener = new Http2BigListener(); + listener.finish = resolve; + chan.asyncOpen(listener); + }); +} + +async function test_http2_huge_suspended(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/huge`); + return new Promise(resolve => { + var listener = new Http2HugeSuspendedListener(); + listener.finish = resolve; + chan.asyncOpen(listener); + chan.suspend(); + do_timeout(500, chan.resume); + }); +} + +// Support for doing a POST +function do_post(content, chan, listener, method) { + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = content; + + var uchan = chan.QueryInterface(Ci.nsIUploadChannel); + uchan.setUploadStream(stream, "text/plain", stream.available()); + + chan.requestMethod = method; + + chan.asyncOpen(listener); +} + +// Make sure we can do a simple POST +async function test_http2_post(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/post`); + var p = new Promise(resolve => { + var listener = new Http2PostListener(md5s[0]); + listener.finish = resolve; + do_post(posts[0], chan, listener, "POST"); + }); + return p; +} + +async function test_http2_empty_post(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/post`); + var p = new Promise(resolve => { + var listener = new Http2PostListener("0"); + listener.finish = resolve; + do_post("", chan, listener, "POST"); + }); + return p; +} + +// Make sure we can do a simple PATCH +async function test_http2_patch(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/patch`); + return new Promise(resolve => { + var listener = new Http2PostListener(md5s[0]); + listener.finish = resolve; + do_post(posts[0], chan, listener, "PATCH"); + }); +} + +// Make sure we can do a POST that covers more than 2 frames +async function test_http2_post_big(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/post`); + return new Promise(resolve => { + var listener = new Http2PostListener(md5s[1]); + listener.finish = resolve; + do_post(posts[1], chan, listener, "POST"); + }); +} + +// When a http proxy is used alt-svc is disable. Therefore if withProxy is true, +// try numberOfTries times to connect and make sure that alt-svc is not use and we never +// connect to the HTTP/2 server. +var altsvcClientListener = function ( + finish, + httpserv, + httpserv2, + withProxy, + numberOfTries +) { + this.finish = finish; + this.httpserv = httpserv; + this.httpserv2 = httpserv2; + this.withProxy = withProxy; + this.numberOfTries = numberOfTries; +}; + +altsvcClientListener.prototype = { + onStartRequest: function test_onStartR(request) { + Assert.equal(request.status, Cr.NS_OK); + }, + + onDataAvailable: function test_ODA(request, stream, offset, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function test_onStopR(request, status) { + var isHttp2Connection = checkIsHttp2( + request.QueryInterface(Ci.nsIHttpChannel) + ); + if (!isHttp2Connection) { + dump("/altsvc1 not over h2 yet - retry\n"); + if (this.withProxy && this.numberOfTries == 0) { + request.QueryInterface(Ci.nsIProxiedChannel); + var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + this.finish({ httpProxyConnectResponseCode }); + return; + } + let chan = makeHTTPChannel( + `http://foo.example.com:${this.httpserv}/altsvc1`, + this.withProxy + ).QueryInterface(Ci.nsIHttpChannel); + // we use this header to tell the server to issue a altsvc frame for the + // speficied origin we will use in the next part of the test + chan.setRequestHeader( + "x-redirect-origin", + `http://foo.example.com:${this.httpserv2}`, + false + ); + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen( + new altsvcClientListener( + this.finish, + this.httpserv, + this.httpserv2, + this.withProxy, + this.numberOfTries - 1 + ) + ); + } else { + Assert.ok(isHttp2Connection); + let chan = makeHTTPChannel( + `http://foo.example.com:${this.httpserv2}/altsvc2` + ).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen( + new altsvcClientListener2(this.finish, this.httpserv, this.httpserv2) + ); + } + }, +}; + +var altsvcClientListener2 = function (finish, httpserv, httpserv2) { + this.finish = finish; + this.httpserv = httpserv; + this.httpserv2 = httpserv2; +}; + +altsvcClientListener2.prototype = { + onStartRequest: function test_onStartR(request) { + Assert.equal(request.status, Cr.NS_OK); + }, + + onDataAvailable: function test_ODA(request, stream, offset, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function test_onStopR(request, status) { + var isHttp2Connection = checkIsHttp2( + request.QueryInterface(Ci.nsIHttpChannel) + ); + if (!isHttp2Connection) { + dump("/altsvc2 not over h2 yet - retry\n"); + var chan = makeHTTPChannel( + `http://foo.example.com:${this.httpserv2}/altsvc2` + ).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen( + new altsvcClientListener2(this.finish, this.httpserv, this.httpserv2) + ); + } else { + Assert.ok(isHttp2Connection); + request.QueryInterface(Ci.nsIProxiedChannel); + var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + this.finish({ httpProxyConnectResponseCode }); + } + }, +}; + +async function test_http2_altsvc(httpserv, httpserv2, withProxy) { + var chan = makeHTTPChannel( + `http://foo.example.com:${httpserv}/altsvc1`, + withProxy + ).QueryInterface(Ci.nsIHttpChannel); + return new Promise(resolve => { + var numberOfTries = 0; + if (withProxy) { + numberOfTries = 20; + } + chan.asyncOpen( + new altsvcClientListener( + resolve, + httpserv, + httpserv2, + withProxy, + numberOfTries + ) + ); + }); +} + +var Http2PushApiListener = function (finish, serverPort) { + this.finish = finish; + this.serverPort = serverPort; +}; + +Http2PushApiListener.prototype = { + checksPending: 9, // 4 onDataAvailable and 5 onStop + + getInterface(aIID) { + return this.QueryInterface(aIID); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIHttpPushListener", + "nsIStreamListener", + ]), + + // nsIHttpPushListener + onPush: function onPush(associatedChannel, pushChannel) { + Assert.equal( + associatedChannel.originalURI.spec, + "https://localhost:" + this.serverPort + "/pushapi1" + ); + Assert.equal(pushChannel.getRequestHeader("x-pushed-request"), "true"); + + pushChannel.asyncOpen(this); + if ( + pushChannel.originalURI.spec == + "https://localhost:" + this.serverPort + "/pushapi1/2" + ) { + pushChannel.cancel(Cr.NS_ERROR_ABORT); + } else if ( + pushChannel.originalURI.spec == + "https://localhost:" + this.serverPort + "/pushapi1/3" + ) { + Assert.ok(pushChannel.getRequestHeader("Accept-Encoding").includes("br")); + } + }, + + // normal Channel listeners + onStartRequest: function pushAPIOnStart(request) {}, + + onDataAvailable: function pushAPIOnDataAvailable( + request, + stream, + offset, + cnt + ) { + Assert.notEqual( + request.originalURI.spec, + `https://localhost:${this.serverPort}/pushapi1/2` + ); + + var data = read_stream(stream, cnt); + + if ( + request.originalURI.spec == + `https://localhost:${this.serverPort}/pushapi1` + ) { + Assert.equal(data[0], "0"); + --this.checksPending; + } else if ( + request.originalURI.spec == + `https://localhost:${this.serverPort}/pushapi1/1` + ) { + Assert.equal(data[0], "1"); + --this.checksPending; // twice + } else if ( + request.originalURI.spec == + `https://localhost:${this.serverPort}/pushapi1/3` + ) { + Assert.equal(data[0], "3"); + --this.checksPending; + } else { + Assert.equal(true, false); + } + }, + + onStopRequest: function test_onStopR(request, status) { + if ( + request.originalURI.spec == + `https://localhost:${this.serverPort}/pushapi1/2` + ) { + Assert.equal(request.status, Cr.NS_ERROR_ABORT); + } else { + Assert.equal(request.status, Cr.NS_OK); + } + + --this.checksPending; // 5 times - one for each push plus the pull + if (!this.checksPending) { + request.QueryInterface(Ci.nsIProxiedChannel); + var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + this.finish({ httpProxyConnectResponseCode }); + } + }, +}; + +// pushAPI testcase 1 expects +// 1 to pull /pushapi1 with 0 +// 2 to see /pushapi1/1 with 1 +// 3 to see /pushapi1/1 with 1 (again) +// 4 to see /pushapi1/2 that it will cancel +// 5 to see /pushapi1/3 with 3 with brotli + +async function test_http2_pushapi_1(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/pushapi1`); + chan.loadGroup = loadGroup; + return new Promise(resolve => { + var listener = new Http2PushApiListener(resolve, serverPort); + chan.notificationCallbacks = listener; + chan.asyncOpen(listener); + }); +} + +var WrongSuiteListener = function () {}; + +WrongSuiteListener.prototype = new Http2CheckListener(); +WrongSuiteListener.prototype.shouldBeHttp2 = false; +WrongSuiteListener.prototype.onStopRequest = function (request, status) { + Services.prefs.setBoolPref( + "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", + true + ); + Services.prefs.clearUserPref("security.tls.version.max"); + Http2CheckListener.prototype.onStopRequest.call(this, request, status); +}; + +// test that we use h1 without the mandatory cipher suite available when +// offering at most tls1.2 +async function test_http2_wrongsuite_tls12(serverPort) { + Services.prefs.setBoolPref( + "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", + false + ); + Services.prefs.setIntPref("security.tls.version.max", 3); + var chan = makeHTTPChannel(`https://localhost:${serverPort}/wrongsuite`); + chan.loadFlags = + Ci.nsIRequest.LOAD_FRESH_CONNECTION | + Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return new Promise(resolve => { + var listener = new WrongSuiteListener(); + listener.finish = resolve; + chan.asyncOpen(listener); + }); +} + +// test that we use h2 when offering tls1.3 or higher regardless of if the +// mandatory cipher suite is available +async function test_http2_wrongsuite_tls13(serverPort) { + Services.prefs.setBoolPref( + "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", + false + ); + var chan = makeHTTPChannel(`https://localhost:${serverPort}/wrongsuite`); + chan.loadFlags = + Ci.nsIRequest.LOAD_FRESH_CONNECTION | + Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return new Promise(resolve => { + var listener = new WrongSuiteListener(); + listener.finish = resolve; + listener.shouldBeHttp2 = true; + chan.asyncOpen(listener); + }); +} + +async function test_http2_h11required_stream(serverPort) { + var chan = makeHTTPChannel( + `https://localhost:${serverPort}/h11required_stream` + ); + return new Promise(resolve => { + var listener = new Http2CheckListener(); + listener.finish = resolve; + listener.shouldBeHttp2 = false; + chan.asyncOpen(listener); + }); +} + +function H11RequiredSessionListener() {} + +H11RequiredSessionListener.prototype = new Http2CheckListener(); + +H11RequiredSessionListener.prototype.onStopRequest = function ( + request, + status +) { + var streamReused = request.getResponseHeader("X-H11Required-Stream-Ok"); + Assert.equal(streamReused, "yes"); + + Assert.ok(this.onStartRequestFired); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection == this.shouldBeHttp2); + + request.QueryInterface(Ci.nsIProxiedChannel); + var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + this.finish({ httpProxyConnectResponseCode }); +}; + +async function test_http2_h11required_session(serverPort) { + var chan = makeHTTPChannel( + `https://localhost:${serverPort}/h11required_session` + ); + return new Promise(resolve => { + var listener = new H11RequiredSessionListener(); + listener.finish = resolve; + listener.shouldBeHttp2 = false; + chan.asyncOpen(listener); + }); +} + +async function test_http2_retry_rst(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/rstonce`); + return new Promise(resolve => { + var listener = new Http2CheckListener(); + listener.finish = resolve; + chan.asyncOpen(listener); + }); +} + +async function test_http2_continuations(loadGroup, serverPort) { + var chan = makeHTTPChannel( + `https://localhost:${serverPort}/continuedheaders` + ); + chan.loadGroup = loadGroup; + return new Promise(resolve => { + var listener = new Http2ContinuedHeaderListener(); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.notificationCallbacks = listener; + chan.asyncOpen(listener); + }); +} + +function Http2IllegalHpackValidationListener() {} + +Http2IllegalHpackValidationListener.prototype = new Http2CheckListener(); +Http2IllegalHpackValidationListener.prototype.shouldGoAway = false; + +Http2IllegalHpackValidationListener.prototype.onStopRequest = function ( + request, + status +) { + var wentAway = request.getResponseHeader("X-Did-Goaway") === "yes"; + Assert.equal(wentAway, this.shouldGoAway); + + Assert.ok(this.onStartRequestFired); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection == this.shouldBeHttp2); + + request.QueryInterface(Ci.nsIProxiedChannel); + var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + this.finish({ httpProxyConnectResponseCode }); +}; + +function Http2IllegalHpackListener() {} +Http2IllegalHpackListener.prototype = new Http2CheckListener(); +Http2IllegalHpackListener.prototype.shouldGoAway = false; + +Http2IllegalHpackListener.prototype.onStopRequest = function (request, status) { + var chan = makeHTTPChannel( + `https://localhost:${this.serverPort}/illegalhpack_validate` + ); + var listener = new Http2IllegalHpackValidationListener(); + listener.finish = this.finish; + listener.shouldGoAway = this.shouldGoAway; + chan.asyncOpen(listener); +}; + +async function test_http2_illegalhpacksoft(serverPort) { + var chan = makeHTTPChannel( + `https://localhost:${serverPort}/illegalhpacksoft` + ); + return new Promise(resolve => { + var listener = new Http2IllegalHpackListener(); + listener.finish = resolve; + listener.serverPort = serverPort; + listener.shouldGoAway = false; + listener.shouldSucceed = false; + chan.asyncOpen(listener); + }); +} + +async function test_http2_illegalhpackhard(serverPort) { + var chan = makeHTTPChannel( + `https://localhost:${serverPort}/illegalhpackhard` + ); + return new Promise(resolve => { + var listener = new Http2IllegalHpackListener(); + listener.finish = resolve; + listener.serverPort = serverPort; + listener.shouldGoAway = true; + listener.shouldSucceed = false; + chan.asyncOpen(listener); + }); +} + +async function test_http2_folded_header(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/foldedheader`); + chan.loadGroup = loadGroup; + return new Promise(resolve => { + var listener = new Http2CheckListener(); + listener.finish = resolve; + listener.shouldSucceed = false; + chan.asyncOpen(listener); + }); +} + +async function test_http2_empty_data(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/emptydata`); + return new Promise(resolve => { + var listener = new Http2CheckListener(); + listener.finish = resolve; + chan.asyncOpen(listener); + }); +} + +async function test_http2_push_firstparty1(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/push`); + chan.loadGroup = loadGroup; + chan.loadInfo.originAttributes = { firstPartyDomain: "foo.com" }; + return new Promise(resolve => { + var listener = new Http2PushListener(true); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.asyncOpen(listener); + }); +} + +async function test_http2_push_firstparty2(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/push.js`); + chan.loadGroup = loadGroup; + chan.loadInfo.originAttributes = { firstPartyDomain: "bar.com" }; + return new Promise(resolve => { + var listener = new Http2PushListener(false); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.asyncOpen(listener); + }); +} + +async function test_http2_push_firstparty3(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/push.js`); + chan.loadGroup = loadGroup; + chan.loadInfo.originAttributes = { firstPartyDomain: "foo.com" }; + return new Promise(resolve => { + var listener = new Http2PushListener(true); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.asyncOpen(listener); + }); +} + +async function test_http2_push_userContext1(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/push`); + chan.loadGroup = loadGroup; + chan.loadInfo.originAttributes = { userContextId: 1 }; + return new Promise(resolve => { + var listener = new Http2PushListener(true); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.asyncOpen(listener); + }); +} + +async function test_http2_push_userContext2(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/push.js`); + chan.loadGroup = loadGroup; + chan.loadInfo.originAttributes = { userContextId: 2 }; + return new Promise(resolve => { + var listener = new Http2PushListener(false); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.asyncOpen(listener); + }); +} + +async function test_http2_push_userContext3(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/push.js`); + chan.loadGroup = loadGroup; + chan.loadInfo.originAttributes = { userContextId: 1 }; + return new Promise(resolve => { + var listener = new Http2PushListener(true); + listener.finish = resolve; + listener.serverPort = serverPort; + chan.asyncOpen(listener); + }); +} + +async function test_http2_status_phrase(serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/statusphrase`); + return new Promise(resolve => { + var listener = new Http2CheckListener(); + listener.finish = resolve; + listener.shouldSucceed = false; + chan.asyncOpen(listener); + }); +} + +var PulledDiskCacheListener = function () {}; +PulledDiskCacheListener.prototype = new Http2CheckListener(); +PulledDiskCacheListener.prototype.EXPECTED_DATA = "this was pulled via h2"; +PulledDiskCacheListener.prototype.readData = ""; +PulledDiskCacheListener.prototype.onDataAvailable = + function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.accum += cnt; + this.readData += read_stream(stream, cnt); + }; +PulledDiskCacheListener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + Assert.equal(this.EXPECTED_DATA, this.readData); + Http2CheckListener.prorotype.onStopRequest.call(this, request, status); +}; + +const DISK_CACHE_DATA = "this is from disk cache"; + +var FromDiskCacheListener = function (finish, loadGroup, serverPort) { + this.finish = finish; + this.loadGroup = loadGroup; + this.serverPort = serverPort; +}; +FromDiskCacheListener.prototype = { + onStartRequestFired: false, + onDataAvailableFired: false, + readData: "", + + onStartRequest: function testOnStartRequest(request) { + this.onStartRequestFired = true; + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code! (" + request.status + ")"); + } + + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.ok(request.requestSucceeded); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + this.readData += read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.equal(this.readData, DISK_CACHE_DATA); + + evict_cache_entries("disk"); + syncWithCacheIOThread(() => { + // Now that we know the entry is out of the disk cache, check to make sure + // we don't have this hiding in the push cache somewhere - if we do, it + // didn't get cancelled, and we have a bug. + var chan = makeHTTPChannel( + `https://localhost:${this.serverPort}/diskcache` + ); + var listener = new PulledDiskCacheListener(); + listener.finish = this.finish; + chan.loadGroup = this.loadGroup; + chan.asyncOpen(listener); + }); + }, +}; + +var Http2DiskCachePushListener = function () {}; +Http2DiskCachePushListener.prototype = new Http2CheckListener(); + +Http2DiskCachePushListener.onStopRequest = function (request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection == this.shouldBeHttp2); + + // Now we need to open a channel to ensure we get data from the disk cache + // for the pushed item, instead of from the push cache. + var chan = makeHTTPChannel(`https://localhost:${this.serverPort}/diskcache`); + var listener = new FromDiskCacheListener( + this.finish, + this.loadGroup, + this.serverPort + ); + chan.loadGroup = this.loadGroup; + chan.asyncOpen(listener); +}; + +function continue_test_http2_disk_cache_push( + status, + entry, + finish, + loadGroup, + serverPort +) { + // TODO - store stuff in cache entry, then open an h2 channel that will push + // this, once that completes, open a channel for the cache entry we made and + // ensure it came from disk cache, not the push cache. + var outputStream = entry.openOutputStream(0, -1); + outputStream.write(DISK_CACHE_DATA, DISK_CACHE_DATA.length); + + // Now we open our URL that will push data for the URL above + var chan = makeHTTPChannel(`https://localhost:${serverPort}/pushindisk`); + var listener = new Http2DiskCachePushListener(); + listener.finish = finish; + listener.loadGroup = loadGroup; + listener.serverPort = serverPort; + chan.loadGroup = loadGroup; + chan.asyncOpen(listener); +} + +async function test_http2_disk_cache_push(loadGroup, serverPort) { + return new Promise(resolve => { + asyncOpenCacheEntry( + `https://localhost:${serverPort}/diskcache`, + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function (status, entry) { + continue_test_http2_disk_cache_push( + status, + entry, + resolve, + loadGroup, + serverPort + ); + }, + false + ); + }); +} + +var Http2DoublepushListener = function () {}; +Http2DoublepushListener.prototype = new Http2CheckListener(); +Http2DoublepushListener.prototype.onStopRequest = function (request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection == this.shouldBeHttp2); + + var chan = makeHTTPChannel( + `https://localhost:${this.serverPort}/doublypushed` + ); + var listener = new Http2DoublypushedListener(); + listener.finish = this.finish; + chan.loadGroup = this.loadGroup; + chan.asyncOpen(listener); +}; + +var Http2DoublypushedListener = function () {}; +Http2DoublypushedListener.prototype = new Http2CheckListener(); +Http2DoublypushedListener.prototype.readData = ""; +Http2DoublypushedListener.prototype.onDataAvailable = function ( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.accum += cnt; + this.readData += read_stream(stream, cnt); +}; +Http2DoublypushedListener.prototype.onStopRequest = function (request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.equal(this.readData, "pushed"); + + request.QueryInterface(Ci.nsIProxiedChannel); + let httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; + this.finish({ httpProxyConnectResponseCode }); +}; + +function test_http2_doublepush(loadGroup, serverPort) { + var chan = makeHTTPChannel(`https://localhost:${serverPort}/doublepush`); + return new Promise(resolve => { + var listener = new Http2DoublepushListener(); + listener.finish = resolve; + listener.loadGroup = loadGroup; + listener.serverPort = serverPort; + chan.loadGroup = loadGroup; + chan.asyncOpen(listener); + }); +} diff --git a/netwerk/test/unit/perftest.ini b/netwerk/test/unit/perftest.ini new file mode 100644 index 0000000000..789d6ae3e9 --- /dev/null +++ b/netwerk/test/unit/perftest.ini @@ -0,0 +1 @@ +[test_http3_perf.js] diff --git a/netwerk/test/unit/proxy-ca.pem b/netwerk/test/unit/proxy-ca.pem new file mode 100644 index 0000000000..5325d8cbd2 --- /dev/null +++ b/netwerk/test/unit/proxy-ca.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC1DCCAbygAwIBAgIUW4p+/QPIt/MX8PWl1HdqbTSfjakwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOIFByb3h5IFRlc3QgQ0EwIhgPMjAyMjAxMDEwMDAwMDBa +GA8yMDMyMDEwMTAwMDAwMFowGTEXMBUGA1UEAwwOIFByb3h5IFRlc3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6iFGoRI4W1kH9braIBjYQPTwT +2erkNUq07PVoV2wke8HHJajg2B+9sZwGm24ahvJr4q9adWtqZHEIeqVap0WH9xzV +JJwCfs1D/B5p0DggKZOrIMNJ5Nu5TMJrbA7tFYIP8X6taRqx0wI6iypB7qdw4A8N +jf1mCyuwJJKkfbmIYXmQsVeQPdI7xeC4SB+oN9OIQ+8nFthVt2Zaqn4CkC86exCA +BiTMHGyXrZZhW7filhLAdTGjDJHdtMr3/K0dJdMJ77kXDqdo4bN7LyJvaeO0ipVh +He4m1iWdq5EITjbLHCQELL8Wiy/l8Y+ZFzG4s/5JI/pyUcQx1QOs2hgKNe2NAgMB +AAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIeZDX+A8ZhI +NU+wg2vTMKr5hyd0cOVUgOOWUGmATrgAMuo9Asn29SNcI61nGTrUpDBuDCy8OTQf +J7Gva5eLmS/c+INpRtLzcCKvkd06bexSKk8naUTuFwtwtS0WQwSV20yUf9mR+UcO +60U9F2r7dHfgFqXlNhQ1AngXkfMOlrCWw50CyMj7y9fOeJ22Q0JDXzK2UU64tWhi +e22fSfFCdjRcIPiqdG+BHNQe2M6DYPMgrrEnRqq/3mJsf886FxR1AhqkhYS/tkLZ +TGSrETajIK62v3qY9LNB8iWv2e0lj0pZlPsTmUWysIXc3fAF6RIu3T8Ypxm5i6sw +OfmaaMxPV20= +-----END CERTIFICATE----- diff --git a/netwerk/test/unit/proxy-ca.pem.certspec b/netwerk/test/unit/proxy-ca.pem.certspec new file mode 100644 index 0000000000..4e0279143d --- /dev/null +++ b/netwerk/test/unit/proxy-ca.pem.certspec @@ -0,0 +1,4 @@ +issuer: Proxy Test CA +subject: Proxy Test CA +validity:20220101-20320101 +extension:basicConstraints:cA, diff --git a/netwerk/test/unit/socks_client_subprocess.js b/netwerk/test/unit/socks_client_subprocess.js new file mode 100644 index 0000000000..3fb64dbc78 --- /dev/null +++ b/netwerk/test/unit/socks_client_subprocess.js @@ -0,0 +1,101 @@ +/* global arguments */ + +"use strict"; + +var CC = Components.Constructor; + +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +const ProtocolProxyService = CC( + "@mozilla.org/network/protocol-proxy-service;1", + "nsIProtocolProxyService" +); +var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( + Ci.nsISocketTransportService +); + +function waitForStream(stream, streamType) { + return new Promise((resolve, reject) => { + stream = stream.QueryInterface(streamType); + if (!stream) { + reject("stream didn't implement given stream type"); + } + let currentThread = + Cc["@mozilla.org/thread-manager;1"].getService().currentThread; + stream.asyncWait( + stream => { + resolve(stream); + }, + 0, + 0, + currentThread + ); + }); +} + +async function launchConnection( + socks_vers, + socks_port, + dest_host, + dest_port, + dns +) { + let pi_flags = 0; + if (dns == "remote") { + pi_flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST; + } + + let pps = new ProtocolProxyService(); + let pi = pps.newProxyInfo( + socks_vers, + "localhost", + socks_port, + "", + "", + pi_flags, + -1, + null + ); + let trans = sts.createTransport([], dest_host, dest_port, pi, null); + let input = trans.openInputStream(0, 0, 0); + let output = trans.openOutputStream(0, 0, 0); + input = await waitForStream(input, Ci.nsIAsyncInputStream); + let bin = new BinaryInputStream(input); + let data = bin.readBytes(5); + let response; + if (data == "PING!") { + print("client: got ping, sending pong."); + response = "PONG!"; + } else { + print("client: wrong data from server:", data); + response = "Error: wrong data received."; + } + output = await waitForStream(output, Ci.nsIAsyncOutputStream); + output.write(response, response.length); + output.close(); + input.close(); +} + +async function run(args) { + for (let arg of args) { + print("client: running test", arg); + let test = arg.split("|"); + await launchConnection( + test[0], + parseInt(test[1]), + test[2], + parseInt(test[3]), + test[4] + ); + } +} + +var satisfied = false; +run(arguments).then(() => (satisfied = true)); +var mainThread = Cc["@mozilla.org/thread-manager;1"].getService().mainThread; +while (!satisfied) { + mainThread.processNextEvent(true); +} diff --git a/netwerk/test/unit/test_1073747.js b/netwerk/test/unit/test_1073747.js new file mode 100644 index 0000000000..dd8c597113 --- /dev/null +++ b/netwerk/test/unit/test_1073747.js @@ -0,0 +1,42 @@ +// Test based on submitted one from Peter B Shalimoff + +"use strict"; + +var test = function (s, funcName) { + function Arg() {} + Arg.prototype.toString = function () { + info("Testing " + funcName + " with null args"); + return this.value; + }; + // create a generic arg lits of null, -1, and 10 nulls + var args = [s, -1]; + for (var i = 0; i < 10; ++i) { + args.push(new Arg()); + } + var up = Cc["@mozilla.org/network/url-parser;1?auth=maybe"].getService( + Ci.nsIURLParser + ); + try { + up[funcName].apply(up, args); + return args; + } catch (x) { + Assert.ok(true); // make sure it throws an exception instead of crashing + return x; + } +}; +var s = null; +var funcs = [ + "parseAuthority", + "parseFileName", + "parseFilePath", + "parsePath", + "parseServerInfo", + "parseURL", + "parseUserInfo", +]; + +function run_test() { + funcs.forEach(function (f) { + test(s, f); + }); +} diff --git a/netwerk/test/unit/test_304_headers.js b/netwerk/test/unit/test_304_headers.js new file mode 100644 index 0000000000..4718925c35 --- /dev/null +++ b/netwerk/test/unit/test_304_headers.js @@ -0,0 +1,89 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return `http://localhost:${httpServer.identity.primaryPort}/test`; +}); + +let httpServer = null; + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function contentHandler(metadata, response) { + response.seizePower(); + let etag = ""; + try { + etag = metadata.getHeader("If-None-Match"); + } catch (ex) {} + + if (etag == "test-etag1") { + response.write("HTTP/1.1 304 Not Modified\r\n"); + + response.write("Link: <ref>; param1=value1\r\n"); + response.write("Link: <ref2>; param2=value2\r\n"); + response.write("Link: <ref3>; param1=value1\r\n"); + response.write("\r\n"); + response.finish(); + return; + } + + response.write("HTTP/1.1 200 OK\r\n"); + + response.write("ETag: test-etag1\r\n"); + response.write("Link: <ref>; param1=value1\r\n"); + response.write("Link: <ref2>; param2=value2\r\n"); + response.write("Link: <ref3>; param1=value1\r\n"); + response.write("\r\n"); + response.finish(); +} + +add_task(async function test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/test", contentHandler); + httpServer.start(-1); + registerCleanupFunction(async () => { + await httpServer.stop(); + }); + + let chan = make_channel(Services.io.newURI(URL)); + chan.requestMethod = "HEAD"; + await new Promise(resolve => { + chan.asyncOpen({ + onStopRequest(req, status) { + equal(status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + equal( + req.QueryInterface(Ci.nsIHttpChannel).getResponseHeader("Link"), + "<ref>; param1=value1, <ref2>; param2=value2, <ref3>; param1=value1" + ); + resolve(); + }, + onStartRequest(req) {}, + onDataAvailable() {}, + }); + }); + + chan = make_channel(Services.io.newURI(URL)); + chan.requestMethod = "HEAD"; + await new Promise(resolve => { + chan.asyncOpen({ + onStopRequest(req, status) { + equal(status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + equal( + req.QueryInterface(Ci.nsIHttpChannel).getResponseHeader("Link"), + "<ref>; param1=value1, <ref2>; param2=value2, <ref3>; param1=value1" + ); + resolve(); + }, + onStartRequest(req) {}, + onDataAvailable() {}, + }); + }); +}); diff --git a/netwerk/test/unit/test_304_responses.js b/netwerk/test/unit/test_304_responses.js new file mode 100644 index 0000000000..a1a6bea0c9 --- /dev/null +++ b/netwerk/test/unit/test_304_responses.js @@ -0,0 +1,90 @@ +"use strict"; +// https://bugzilla.mozilla.org/show_bug.cgi?id=761228 + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; +const testFileName = "test_customConditionalRequest_304"; +const basePath = "/" + testFileName + "/"; + +XPCOMUtils.defineLazyGetter(this, "baseURI", function () { + return URL + basePath; +}); + +const unexpected304 = "unexpected304"; +const existingCached304 = "existingCached304"; + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function alwaysReturn304Handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + response.setHeader("Returned-From-Handler", "1"); +} + +function run_test() { + evict_cache_entries(); + + httpServer = new HttpServer(); + httpServer.registerPathHandler( + basePath + unexpected304, + alwaysReturn304Handler + ); + httpServer.registerPathHandler( + basePath + existingCached304, + alwaysReturn304Handler + ); + httpServer.start(-1); + run_next_test(); +} + +function consume304(request, buffer) { + request.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(request.responseStatus, 304); + Assert.equal(request.getResponseHeader("Returned-From-Handler"), "1"); + run_next_test(); +} + +// Test that we return a 304 response to the caller when we are not expecting +// a 304 response (i.e. when the server shouldn't have sent us one). +add_test(function test_unexpected_304() { + var chan = make_channel(baseURI + unexpected304); + chan.asyncOpen(new ChannelListener(consume304, null)); +}); + +// Test that we can cope with a 304 response that was (erroneously) stored in +// the cache. +add_test(function test_304_stored_in_cache() { + asyncOpenCacheEntry( + baseURI + existingCached304, + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function (entryStatus, cacheEntry) { + cacheEntry.setMetaDataElement("request-method", "GET"); + cacheEntry.setMetaDataElement( + "response-head", + // eslint-disable-next-line no-useless-concat + "HTTP/1.1 304 Not Modified\r\n" + "\r\n" + ); + cacheEntry.metaDataReady(); + cacheEntry.close(); + + var chan = make_channel(baseURI + existingCached304); + + // make it a custom conditional request + chan.QueryInterface(Ci.nsIHttpChannel); + chan.setRequestHeader("If-None-Match", '"foo"', false); + + chan.asyncOpen(new ChannelListener(consume304, null)); + } + ); +}); diff --git a/netwerk/test/unit/test_307_redirect.js b/netwerk/test/unit/test_307_redirect.js new file mode 100644 index 0000000000..0f14e1da40 --- /dev/null +++ b/netwerk/test/unit/test_307_redirect.js @@ -0,0 +1,93 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "uri", function () { + return URL + "/redirect"; +}); + +XPCOMUtils.defineLazyGetter(this, "noRedirectURI", function () { + return URL + "/content"; +}); + +var httpserver = null; + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const requestBody = "request body"; + +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily"); + response.setHeader("Location", noRedirectURI, false); +} + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.writeFrom( + metadata.bodyInputStream, + metadata.bodyInputStream.available() + ); +} + +function noRedirectStreamObserver(request, buffer) { + Assert.equal(buffer, requestBody); + var chan = make_channel(uri); + var uploadStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + uploadStream.setData(requestBody, requestBody.length); + chan + .QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(uploadStream, "text/plain", -1); + chan.asyncOpen(new ChannelListener(noHeaderStreamObserver, null)); +} + +function noHeaderStreamObserver(request, buffer) { + Assert.equal(buffer, requestBody); + var chan = make_channel(uri); + var uploadStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + var streamBody = + "Content-Type: text/plain\r\n" + + "Content-Length: " + + requestBody.length + + "\r\n\r\n" + + requestBody; + uploadStream.setData(streamBody, streamBody.length); + chan + .QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(uploadStream, "", -1); + chan.asyncOpen(new ChannelListener(headerStreamObserver, null)); +} + +function headerStreamObserver(request, buffer) { + Assert.equal(buffer, requestBody); + httpserver.stop(do_test_finished); +} + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/redirect", redirectHandler); + httpserver.registerPathHandler("/content", contentHandler); + httpserver.start(-1); + + Services.prefs.setBoolPref("network.http.prompt-temp-redirect", false); + + var chan = make_channel(noRedirectURI); + var uploadStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + uploadStream.setData(requestBody, requestBody.length); + chan + .QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(uploadStream, "text/plain", -1); + chan.asyncOpen(new ChannelListener(noRedirectStreamObserver, null)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_421.js b/netwerk/test/unit/test_421.js new file mode 100644 index 0000000000..1017701f55 --- /dev/null +++ b/netwerk/test/unit/test_421.js @@ -0,0 +1,63 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var testpath = "/421"; +var httpbody = "0123456789"; +var channel; + +function run_test() { + setup_test(); + do_test_pending(); +} + +function setup_test() { + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + + channel = setupChannel(testpath); + + channel.asyncOpen(new ChannelListener(checkRequestResponse, channel)); +} + +function setupChannel(path) { + var chan = NetUtil.newChannel({ + uri: URL + path, + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + return chan; +} + +var iters = 0; + +function serverHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + + if (!iters) { + response.setStatusLine("1.1", 421, "Not Authoritative " + iters); + } else { + response.setStatusLine("1.1", 200, "OK"); + } + ++iters; + + response.bodyOutputStream.write(httpbody, httpbody.length); +} + +function checkRequestResponse(request, data, context) { + Assert.equal(channel.responseStatus, 200); + Assert.equal(channel.responseStatusText, "OK"); + Assert.ok(channel.requestSucceeded); + + Assert.equal(channel.contentType, "text/plain"); + Assert.equal(channel.contentLength, httpbody.length); + Assert.equal(data, httpbody); + + httpserver.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_MIME_params.js b/netwerk/test/unit/test_MIME_params.js new file mode 100644 index 0000000000..9ba34282e0 --- /dev/null +++ b/netwerk/test/unit/test_MIME_params.js @@ -0,0 +1,798 @@ +/** + * Tests for parsing header fields using the syntax used in + * Content-Disposition and Content-Type + * + * See also https://bugzilla.mozilla.org/show_bug.cgi?id=609667 + */ + +"use strict"; + +var BS = "\\"; +var DQUOTE = '"'; + +// Test array: +// - element 0: "Content-Disposition" header to test +// under MIME (email): +// - element 1: correct value returned for disposition-type (empty param name) +// - element 2: correct value for filename returned +// under HTTP: +// (currently supports continuations; expected results without continuations +// are commented out for now) +// - element 3: correct value returned for disposition-type (empty param name) +// - element 4: correct value for filename returned +// +// 3 and 4 may be left out if they are identical + +var tests = [ + // No filename parameter: return nothing + ["attachment;", "attachment", Cr.NS_ERROR_INVALID_ARG], + + // basic + ["attachment; filename=basic", "attachment", "basic"], + + // extended + ["attachment; filename*=UTF-8''extended", "attachment", "extended"], + + // prefer extended to basic (bug 588781) + [ + "attachment; filename=basic; filename*=UTF-8''extended", + "attachment", + "extended", + ], + + // prefer extended to basic (bug 588781) + [ + "attachment; filename*=UTF-8''extended; filename=basic", + "attachment", + "extended", + ], + + // use first basic value (invalid; error recovery) + ["attachment; filename=first; filename=wrong", "attachment", "first"], + + // old school bad HTTP servers: missing 'attachment' or 'inline' + // (invalid; error recovery) + ["filename=old", "filename=old", "old"], + + ["attachment; filename*=UTF-8''extended", "attachment", "extended"], + + // continuations not part of RFC 5987 (bug 610054) + [ + "attachment; filename*0=foo; filename*1=bar", + "attachment", + "foobar", + /* "attachment", Cr.NS_ERROR_INVALID_ARG */ + ], + + // Return first continuation (invalid; error recovery) + [ + "attachment; filename*0=first; filename*0=wrong; filename=basic", + "attachment", + "first", + /* "attachment", "basic" */ + ], + + // Only use correctly ordered continuations (invalid; error recovery) + [ + "attachment; filename*0=first; filename*1=second; filename*0=wrong", + "attachment", + "firstsecond", + /* "attachment", Cr.NS_ERROR_INVALID_ARG */ + ], + + // prefer continuation to basic (unless RFC 5987) + [ + "attachment; filename=basic; filename*0=foo; filename*1=bar", + "attachment", + "foobar", + /* "attachment", "basic" */ + ], + + // Prefer extended to basic and/or (broken or not) continuation + // (invalid; error recovery) + [ + "attachment; filename=basic; filename*0=first; filename*0=wrong; filename*=UTF-8''extended", + "attachment", + "extended", + ], + + // RFC 2231 not clear on correct outcome: we prefer non-continued extended + // (invalid; error recovery) + [ + "attachment; filename=basic; filename*=UTF-8''extended; filename*0=foo; filename*1=bar", + "attachment", + "extended", + ], + + // Gaps should result in returning only value until gap hit + // (invalid; error recovery) + [ + "attachment; filename*0=foo; filename*2=bar", + "attachment", + "foo", + /* "attachment", Cr.NS_ERROR_INVALID_ARG */ + ], + + // Don't allow leading 0's (*01) (invalid; error recovery) + [ + "attachment; filename*0=foo; filename*01=bar", + "attachment", + "foo", + /* "attachment", Cr.NS_ERROR_INVALID_ARG */ + ], + + // continuations should prevail over non-extended (unless RFC 5987) + [ + "attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" + + " filename*1=line;\r\n" + + " filename*2*=%20extended", + "attachment", + "multiline extended", + /* "attachment", "basic" */ + ], + + // Gaps should result in returning only value until gap hit + // (invalid; error recovery) + [ + "attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" + + " filename*1=line;\r\n" + + " filename*3*=%20extended", + "attachment", + "multiline", + /* "attachment", "basic" */ + ], + + // First series, only please, and don't slurp up higher elements (*2 in this + // case) from later series into earlier one (invalid; error recovery) + [ + "attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" + + " filename*1=line;\r\n" + + " filename*0*=UTF-8''wrong;\r\n" + + " filename*1=bad;\r\n" + + " filename*2=evil", + "attachment", + "multiline", + /* "attachment", "basic" */ + ], + + // RFC 2231 not clear on correct outcome: we prefer non-continued extended + // (invalid; error recovery) + [ + "attachment; filename=basic; filename*0=UTF-8''multi\r\n;" + + " filename*=UTF-8''extended;\r\n" + + " filename*1=line;\r\n" + + " filename*2*=%20extended", + "attachment", + "extended", + ], + + // sneaky: if unescaped, make sure we leave UTF-8'' in value + [ + "attachment; filename*0=UTF-8''unescaped;\r\n" + + " filename*1*=%20so%20includes%20UTF-8''%20in%20value", + "attachment", + "UTF-8''unescaped so includes UTF-8'' in value", + /* "attachment", Cr.NS_ERROR_INVALID_ARG */ + ], + + // sneaky: if unescaped, make sure we leave UTF-8'' in value + [ + "attachment; filename=basic; filename*0=UTF-8''unescaped;\r\n" + + " filename*1*=%20so%20includes%20UTF-8''%20in%20value", + "attachment", + "UTF-8''unescaped so includes UTF-8'' in value", + /* "attachment", "basic" */ + ], + + // Prefer basic over invalid continuation + // (invalid; error recovery) + [ + "attachment; filename=basic; filename*1=multi;\r\n" + + " filename*2=line;\r\n" + + " filename*3*=%20extended", + "attachment", + "basic", + ], + + // support digits over 10 + [ + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + + " filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" + + " filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" + + " filename*11=b; filename*12=c;filename*13=d;filename*14=e;filename*15=f\r\n", + "attachment", + "0123456789abcdef", + /* "attachment", "basic" */ + ], + + // support digits over 10 (detect gaps) + [ + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + + " filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" + + " filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" + + " filename*11=b; filename*12=c;filename*14=e\r\n", + "attachment", + "0123456789abc", + /* "attachment", "basic" */ + ], + + // return nothing: invalid + // (invalid; error recovery) + [ + "attachment; filename*1=multi;\r\n" + + " filename*2=line;\r\n" + + " filename*3*=%20extended", + "attachment", + Cr.NS_ERROR_INVALID_ARG, + ], + + // Bug 272541: Empty disposition type treated as "attachment" + + // sanity check + [ + "attachment; filename=foo.html", + "attachment", + "foo.html", + "attachment", + "foo.html", + ], + + // the actual bug + [ + "; filename=foo.html", + Cr.NS_ERROR_FIRST_HEADER_FIELD_COMPONENT_EMPTY, + "foo.html", + Cr.NS_ERROR_FIRST_HEADER_FIELD_COMPONENT_EMPTY, + "foo.html", + ], + + // regression check, but see bug 671204 + [ + "filename=foo.html", + "filename=foo.html", + "foo.html", + "filename=foo.html", + "foo.html", + ], + + // Bug 384571: RFC 2231 parameters not decoded when appearing in reversed order + + // check ordering + [ + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + + " filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" + + " filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" + + " filename*11=b; filename*12=c;filename*13=d;filename*15=f;filename*14=e;\r\n", + "attachment", + "0123456789abcdef", + /* "attachment", "basic" */ + ], + + // check non-digits in sequence numbers + [ + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + + " filename*1a=1\r\n", + "attachment", + "0", + /* "attachment", "basic" */ + ], + + // check duplicate sequence numbers + [ + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + + " filename*0=bad; filename*1=1;\r\n", + "attachment", + "0", + /* "attachment", "basic" */ + ], + + // check overflow + [ + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + + " filename*11111111111111111111111111111111111111111111111111111111111=1", + "attachment", + "0", + /* "attachment", "basic" */ + ], + + // check underflow + [ + // eslint-disable-next-line no-useless-concat + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + " filename*-1=1", + "attachment", + "0", + /* "attachment", "basic" */ + ], + + // check mixed token/quoted-string + [ + 'attachment; filename=basic; filename*0="0";\r\n' + + " filename*1=1;\r\n" + + " filename*2*=%32", + "attachment", + "012", + /* "attachment", "basic" */ + ], + + // check empty sequence number + [ + "attachment; filename=basic; filename**=UTF-8''0\r\n", + "attachment", + "basic", + "attachment", + "basic", + ], + + // Bug 419157: ensure that a MIME parameter with no charset information + // fallbacks to Latin-1 + + [ + "attachment;filename=IT839\x04\xB5(m8)2.pdf;", + "attachment", + "IT839\u0004\u00b5(m8)2.pdf", + ], + + // Bug 588389: unescaping backslashes in quoted string parameters + + // '\"', should be parsed as '"' + [ + "attachment; filename=" + DQUOTE + (BS + DQUOTE) + DQUOTE, + "attachment", + DQUOTE, + ], + + // 'a\"b', should be parsed as 'a"b' + [ + "attachment; filename=" + DQUOTE + "a" + (BS + DQUOTE) + "b" + DQUOTE, + "attachment", + "a" + DQUOTE + "b", + ], + + // '\x', should be parsed as 'x' + ["attachment; filename=" + DQUOTE + (BS + "x") + DQUOTE, "attachment", "x"], + + // test empty param (quoted-string) + ["attachment; filename=" + DQUOTE + DQUOTE, "attachment", ""], + + // test empty param + ["attachment; filename=", "attachment", ""], + + // Bug 601933: RFC 2047 does not apply to parameters (at least in HTTP) + [ + "attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=", + "attachment", + "foo-\u00e4.html", + /* "attachment", "=?ISO-8859-1?Q?foo-=E4.html?=" */ + ], + + [ + 'attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?="', + "attachment", + "foo-\u00e4.html", + /* "attachment", "=?ISO-8859-1?Q?foo-=E4.html?=" */ + ], + + // format sent by GMail as of 2012-07-23 (5987 overrides 2047) + [ + "attachment; filename=\"=?ISO-8859-1?Q?foo-=E4.html?=\"; filename*=UTF-8''5987", + "attachment", + "5987", + ], + + // Bug 651185: double quotes around 2231/5987 encoded param + // Change reverted to backwards compat issues with various web services, + // such as OWA (Bug 703015), plus similar problems in Thunderbird. If this + // is tried again in the future, email probably needs to be special-cased. + + // sanity check + ["attachment; filename*=utf-8''%41", "attachment", "A"], + + // the actual bug + [ + "attachment; filename*=" + DQUOTE + "utf-8''%41" + DQUOTE, + "attachment", + "A", + ], + // previously with the fix for 651185: + // "attachment", Cr.NS_ERROR_INVALID_ARG], + + // Bug 670333: Content-Disposition parser does not require presence of "=" + // in params + + // sanity check + ["attachment; filename*=UTF-8''foo-%41.html", "attachment", "foo-A.html"], + + // the actual bug + [ + "attachment; filename *=UTF-8''foo-%41.html", + "attachment", + Cr.NS_ERROR_INVALID_ARG, + ], + + // the actual bug, without 2231/5987 encoding + ["attachment; filename X", "attachment", Cr.NS_ERROR_INVALID_ARG], + + // sanity check with WS on both sides + ["attachment; filename = foo-A.html", "attachment", "foo-A.html"], + + // Bug 685192: in RFC2231/5987 encoding, a missing charset field should be + // treated as error + + // the actual bug + ["attachment; filename*=''foo", "attachment", "foo"], + // previously with the fix for 692574: + // "attachment", Cr.NS_ERROR_INVALID_ARG], + + // sanity check + ["attachment; filename*=a''foo", "attachment", "foo"], + + // Bug 692574: RFC2231/5987 decoding should not tolerate missing single + // quotes + + // one missing + ["attachment; filename*=UTF-8'foo-%41.html", "attachment", "foo-A.html"], + // previously with the fix for 692574: + // "attachment", Cr.NS_ERROR_INVALID_ARG], + + // both missing + ["attachment; filename*=foo-%41.html", "attachment", "foo-A.html"], + // previously with the fix for 692574: + // "attachment", Cr.NS_ERROR_INVALID_ARG], + + // make sure fallback works + [ + "attachment; filename*=UTF-8'foo-%41.html; filename=bar.html", + "attachment", + "foo-A.html", + ], + // previously with the fix for 692574: + // "attachment", "bar.html"], + + // Bug 693806: RFC2231/5987 encoding: charset information should be treated + // as authoritative + + // UTF-8 labeled ISO-8859-1 + ["attachment; filename*=ISO-8859-1''%c3%a4", "attachment", "\u00c3\u00a4"], + + // UTF-8 labeled ISO-8859-1, but with octets not allowed in ISO-8859-1 + // accepts x82, understands it as Win1252, maps it to Unicode \u20a1 + [ + "attachment; filename*=ISO-8859-1''%e2%82%ac", + "attachment", + "\u00e2\u201a\u00ac", + ], + + // defective UTF-8 + ["attachment; filename*=UTF-8''A%e4B", "attachment", Cr.NS_ERROR_INVALID_ARG], + + // defective UTF-8, with fallback + [ + "attachment; filename*=UTF-8''A%e4B; filename=fallback", + "attachment", + "fallback", + ], + + // defective UTF-8 (continuations), with fallback + [ + "attachment; filename*0*=UTF-8''A%e4B; filename=fallback", + "attachment", + "fallback", + ], + + // check that charsets aren't mixed up + [ + "attachment; filename*0*=ISO-8859-15''euro-sign%3d%a4; filename*=ISO-8859-1''currency-sign%3d%a4", + "attachment", + "currency-sign=\u00a4", + ], + + // same as above, except reversed + [ + "attachment; filename*=ISO-8859-1''currency-sign%3d%a4; filename*0*=ISO-8859-15''euro-sign%3d%a4", + "attachment", + "currency-sign=\u00a4", + ], + + // Bug 704989: add workaround for broken Outlook Web App (OWA) + // attachment handling + + ['attachment; filename*="a%20b"', "attachment", "a b"], + + // Bug 717121: crash nsMIMEHeaderParamImpl::DoParameterInternal + + ['attachment; filename="', "attachment", ""], + + // We used to read past string if last param w/o = and ; + // Note: was only detected on windows PGO builds + ["attachment; filename=foo; trouble", "attachment", "foo"], + + // Same, followed by space, hits another case + ["attachment; filename=foo; trouble ", "attachment", "foo"], + + ["attachment", "attachment", Cr.NS_ERROR_INVALID_ARG], + + // Bug 730574: quoted-string in RFC2231-continuations not handled + + [ + 'attachment; filename=basic; filename*0="foo"; filename*1="\\b\\a\\r.html"', + "attachment", + "foobar.html", + /* "attachment", "basic" */ + ], + + // unmatched escape char + [ + 'attachment; filename=basic; filename*0="foo"; filename*1="\\b\\a\\', + "attachment", + "fooba\\", + /* "attachment", "basic" */ + ], + + // Bug 732369: Content-Disposition parser does not require presence of ";" between params + // optimally, this would not even return the disposition type "attachment" + + [ + "attachment; extension=bla filename=foo", + "attachment", + Cr.NS_ERROR_INVALID_ARG, + ], + + // Bug 1440677 - spaces inside filenames ought to be quoted, but too many + // servers do the wrong thing and most browsers accept this, so we were + // forced to do the same for compat. + ["attachment; filename=foo extension=bla", "attachment", "foo extension=bla"], + + ["attachment filename=foo", "attachment", Cr.NS_ERROR_INVALID_ARG], + + // Bug 777687: handling of broken %escapes + + ["attachment; filename*=UTF-8''f%oo; filename=bar", "attachment", "bar"], + + ["attachment; filename*=UTF-8''foo%; filename=bar", "attachment", "bar"], + + // Bug 783502 - xpcshell test netwerk/test/unit/test_MIME_params.js fails on AddressSanitizer + ['attachment; filename="\\b\\a\\', "attachment", "ba\\"], + + // Bug 1412213 - do continue to parse, behind an empty parameter + ["attachment; ; filename=foo", "attachment", "foo"], + + // Bug 1412213 - do continue to parse, behind a parameter w/o = + ["attachment; badparameter; filename=foo", "attachment", "foo"], + + // Bug 1440677 - spaces inside filenames ought to be quoted, but too many + // servers do the wrong thing and most browsers accept this, so we were + // forced to do the same for compat. + ["attachment; filename=foo bar.html", "attachment", "foo bar.html"], + // Note: we keep the tab character, but later validation will replace with a space, + // as file systems do not like tab characters. + ["attachment; filename=foo\tbar.html", "attachment", "foo\tbar.html"], + // Newlines get stripped completely (in practice, http header parsing may + // munge these into spaces before they get to us, but we should check we deal + // with them either way): + ["attachment; filename=foo\nbar.html", "attachment", "foobar.html"], + ["attachment; filename=foo\r\nbar.html", "attachment", "foobar.html"], + ["attachment; filename=foo\rbar.html", "attachment", "foobar.html"], + + // Trailing rubbish shouldn't matter: + ["attachment; filename=foo bar; garbage", "attachment", "foo bar"], + ["attachment; filename=foo bar; extension=blah", "attachment", "foo bar"], + + // Check that whitespace processing can't crash. + ["attachment; filename = ", "attachment", ""], + + // Bug 1784348 + [ + "attachment; filename=foo.exe\0.pdf", + Cr.NS_ERROR_ILLEGAL_VALUE, + Cr.NS_ERROR_INVALID_ARG, + ], + [ + "attachment; filename=\0\0foo\0", + Cr.NS_ERROR_ILLEGAL_VALUE, + Cr.NS_ERROR_INVALID_ARG, + ], + ["attachment; filename=foo\0\0\0", "attachment", "foo"], + ["attachment; filename=\0\0\0", "attachment", ""], +]; + +var rfc5987paramtests = [ + [ + // basic test + "UTF-8'language'value", + "value", + "language", + Cr.NS_OK, + ], + [ + // percent decoding + "UTF-8''1%202", + "1 2", + "", + Cr.NS_OK, + ], + [ + // UTF-8 + "UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", + "\u00a3 and \u20ac rates", + "", + Cr.NS_OK, + ], + [ + // missing charset + "''abc", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // ISO-8859-1: unsupported + "ISO-8859-1''%A3%20rates", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // unknown charset + "foo''abc", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // missing component + "abc", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // missing component + "'abc", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // illegal chars + "UTF-8''a b", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // broken % escapes + "UTF-8''a%zz", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // broken % escapes + "UTF-8''a%b", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // broken % escapes + "UTF-8''a%", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // broken UTF-8 + "UTF-8''%A3%20rates", + "", + "", + 0x8050000e /* NS_ERROR_UDEC_ILLEGALINPUT */, + ], +]; + +function do_tests(whichRFC) { + var mhp = Cc["@mozilla.org/network/mime-hdrparam;1"].getService( + Ci.nsIMIMEHeaderParam + ); + + var unused = { value: null }; + + for (var i = 0; i < tests.length; ++i) { + dump("Testing #" + i + ": " + tests[i] + "\n"); + + // check disposition type + var expectedDt = + tests[i].length == 3 || whichRFC == 0 ? tests[i][1] : tests[i][3]; + + try { + let result; + + if (whichRFC == 0) { + result = mhp.getParameter(tests[i][0], "", "UTF-8", true, unused); + } else { + result = mhp.getParameterHTTP(tests[i][0], "", "UTF-8", true, unused); + } + + Assert.equal(result, expectedDt); + } catch (e) { + // Tests can also succeed by expecting to fail with given error code + if (e.result) { + // Allow following tests to run by catching exception from do_check_eq() + try { + Assert.equal(e.result, expectedDt); + } catch (e) {} + } + continue; + } + + // check filename parameter + var expectedFn = + tests[i].length == 3 || whichRFC == 0 ? tests[i][2] : tests[i][4]; + + try { + let result; + + if (whichRFC == 0) { + result = mhp.getParameter( + tests[i][0], + "filename", + "UTF-8", + true, + unused + ); + } else { + result = mhp.getParameterHTTP( + tests[i][0], + "filename", + "UTF-8", + true, + unused + ); + } + + Assert.equal(result, expectedFn); + } catch (e) { + // Tests can also succeed by expecting to fail with given error code + if (e.result) { + // Allow following tests to run by catching exception from do_check_eq() + try { + Assert.equal(e.result, expectedFn); + } catch (e) {} + } + continue; + } + } +} + +function test_decode5987Param() { + var mhp = Cc["@mozilla.org/network/mime-hdrparam;1"].getService( + Ci.nsIMIMEHeaderParam + ); + + for (var i = 0; i < rfc5987paramtests.length; ++i) { + dump("Testing #" + i + ": " + rfc5987paramtests[i] + "\n"); + + var lang = {}; + try { + var decoded = mhp.decodeRFC5987Param(rfc5987paramtests[i][0], lang); + if (rfc5987paramtests[i][3] == Cr.NS_OK) { + Assert.equal(rfc5987paramtests[i][1], decoded); + Assert.equal(rfc5987paramtests[i][2], lang.value); + } else { + Assert.equal(rfc5987paramtests[i][3], "instead got: " + decoded); + } + } catch (e) { + Assert.equal(rfc5987paramtests[i][3], e.result); + } + } +} + +function run_test() { + // Test RFC 2231 (complete header field values) + do_tests(0); + + // Test RFC 5987 (complete header field values) + do_tests(1); + + // tests for RFC5987 parameter parsing + test_decode5987Param(); +} diff --git a/netwerk/test/unit/test_NetUtil.js b/netwerk/test/unit/test_NetUtil.js new file mode 100644 index 0000000000..1ebc27d54e --- /dev/null +++ b/netwerk/test/unit/test_NetUtil.js @@ -0,0 +1,802 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 sts=2 et + * 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 file tests the methods on NetUtil.jsm. + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// We need the profile directory so the test harness will clean up our test +// files. +do_get_profile(); + +const OUTPUT_STREAM_CONTRACT_ID = "@mozilla.org/network/file-output-stream;1"; +const SAFE_OUTPUT_STREAM_CONTRACT_ID = + "@mozilla.org/network/safe-file-output-stream;1"; + +//////////////////////////////////////////////////////////////////////////////// +//// Helper Methods + +/** + * Reads the contents of a file and returns it as a string. + * + * @param aFile + * The file to return from. + * @return the contents of the file in the form of a string. + */ +function getFileContents(aFile) { + "use strict"; + + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(aFile, -1, 0, 0); + + let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance( + Ci.nsIConverterInputStream + ); + cstream.init(fstream, "UTF-8", 0, 0); + + let string = {}; + cstream.readString(-1, string); + cstream.close(); + return string.value; +} + +/** + * Tests asynchronously writing a file using NetUtil.asyncCopy. + * + * @param aContractId + * The contract ID to use for the output stream + * @param aDeferOpen + * Whether to use DEFER_OPEN in the output stream. + */ +function async_write_file(aContractId, aDeferOpen) { + do_test_pending(); + + // First, we need an output file to write to. + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("NetUtil-async-test-file.tmp"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + // Then, we need an output stream to our output file. + let ostream = Cc[aContractId].createInstance(Ci.nsIFileOutputStream); + ostream.init( + file, + -1, + -1, + aDeferOpen ? Ci.nsIFileOutputStream.DEFER_OPEN : 0 + ); + + // Finally, we need an input stream to take data from. + const TEST_DATA = "this is a test string"; + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + istream.setData(TEST_DATA, TEST_DATA.length); + + NetUtil.asyncCopy(istream, ostream, function (aResult) { + // Make sure the copy was successful! + Assert.ok(Components.isSuccessCode(aResult)); + + // Check the file contents. + Assert.equal(TEST_DATA, getFileContents(file)); + + // Finish the test. + do_test_finished(); + run_next_test(); + }); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +// Test NetUtil.asyncCopy for all possible buffering scenarios +function test_async_copy() { + // Create a data sample + function make_sample(text) { + let data = []; + for (let i = 0; i <= 100; ++i) { + data.push(text); + } + return data.join(); + } + + // Create an input buffer holding some data + function make_input(isBuffered, data) { + if (isBuffered) { + // String input streams are buffered + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + istream.setData(data, data.length); + return istream; + } + + // File input streams are not buffered, so let's create a file + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("NetUtil-asyncFetch-test-file.tmp"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + let ostream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + ostream.init(file, -1, -1, 0); + ostream.write(data, data.length); + ostream.close(); + + let istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + istream.init(file, -1, 0, 0); + + return istream; + } + + // Create an output buffer holding some data + function make_output(isBuffered) { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("NetUtil-asyncFetch-test-file.tmp"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + let ostream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + ostream.init(file, -1, -1, 0); + + if (!isBuffered) { + return { file, sink: ostream }; + } + + let bstream = Cc[ + "@mozilla.org/network/buffered-output-stream;1" + ].createInstance(Ci.nsIBufferedOutputStream); + bstream.init(ostream, 256); + return { file, sink: bstream }; + } + (async function () { + do_test_pending(); + for (let bufferedInput of [true, false]) { + for (let bufferedOutput of [true, false]) { + let text = + "test_async_copy with " + + (bufferedInput ? "buffered input" : "unbuffered input") + + ", " + + (bufferedOutput ? "buffered output" : "unbuffered output"); + info(text); + let TEST_DATA = "[" + make_sample(text) + "]"; + let source = make_input(bufferedInput, TEST_DATA); + let { file, sink } = make_output(bufferedOutput); + let result = await new Promise(resolve => { + NetUtil.asyncCopy(source, sink, resolve); + }); + + // Make sure the copy was successful! + if (!Components.isSuccessCode(result)) { + do_throw(new Components.Exception("asyncCopy error", result)); + } + + // Check the file contents. + Assert.equal(TEST_DATA, getFileContents(file)); + } + } + + do_test_finished(); + run_next_test(); + })(); +} + +function test_async_write_file() { + async_write_file(OUTPUT_STREAM_CONTRACT_ID); +} + +function test_async_write_file_deferred() { + async_write_file(OUTPUT_STREAM_CONTRACT_ID, true); +} + +function test_async_write_file_safe() { + async_write_file(SAFE_OUTPUT_STREAM_CONTRACT_ID); +} + +function test_async_write_file_safe_deferred() { + async_write_file(SAFE_OUTPUT_STREAM_CONTRACT_ID, true); +} + +function test_newURI_no_spec_throws() { + try { + NetUtil.newURI(); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_newURI() { + // Check that we get the same URI back from the IO service and the utility + // method. + const TEST_URI = "http://mozilla.org"; + let iosURI = Services.io.newURI(TEST_URI); + let NetUtilURI = NetUtil.newURI(TEST_URI); + Assert.ok(iosURI.equals(NetUtilURI)); + + run_next_test(); +} + +function test_newURI_takes_nsIFile() { + // Create a test file that we can pass into NetUtil.newURI + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("NetUtil-test-file.tmp"); + + // Check that we get the same URI back from the IO service and the utility + // method. + let iosURI = Services.io.newFileURI(file); + let NetUtilURI = NetUtil.newURI(file); + Assert.ok(iosURI.equals(NetUtilURI)); + + run_next_test(); +} + +function test_asyncFetch_no_channel() { + try { + NetUtil.asyncFetch(null, function () {}); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_asyncFetch_no_callback() { + try { + NetUtil.asyncFetch({}); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_asyncFetch_with_nsIChannel() { + const TEST_DATA = "this is a test string"; + + // Start the http server, and register our handler. + let server = new HttpServer(); + server.registerPathHandler("/test", function (aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + aResponse.setHeader("Content-Type", "text/plain", false); + aResponse.write(TEST_DATA); + }); + server.start(-1); + + // Create our channel. + let channel = NetUtil.newChannel({ + uri: "http://localhost:" + server.identity.primaryPort + "/test", + loadUsingSystemPrincipal: true, + }); + + // Open our channel asynchronously. + NetUtil.asyncFetch(channel, function (aInputStream, aResult) { + // Check that we had success. + Assert.ok(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + Assert.equal(aInputStream.available(), TEST_DATA.length); + let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + is.init(aInputStream); + let result = is.read(TEST_DATA.length); + Assert.equal(TEST_DATA, result); + + server.stop(run_next_test); + }); +} + +function test_asyncFetch_with_nsIURI() { + const TEST_DATA = "this is a test string"; + + // Start the http server, and register our handler. + let server = new HttpServer(); + server.registerPathHandler("/test", function (aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + aResponse.setHeader("Content-Type", "text/plain", false); + aResponse.write(TEST_DATA); + }); + server.start(-1); + + // Create our URI. + let uri = NetUtil.newURI( + "http://localhost:" + server.identity.primaryPort + "/test" + ); + + // Open our URI asynchronously. + NetUtil.asyncFetch( + { + uri, + loadUsingSystemPrincipal: true, + }, + function (aInputStream, aResult) { + // Check that we had success. + Assert.ok(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + Assert.equal(aInputStream.available(), TEST_DATA.length); + let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + is.init(aInputStream); + let result = is.read(TEST_DATA.length); + Assert.equal(TEST_DATA, result); + + server.stop(run_next_test); + }, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); +} + +function test_asyncFetch_with_string() { + const TEST_DATA = "this is a test string"; + + // Start the http server, and register our handler. + let server = new HttpServer(); + server.registerPathHandler("/test", function (aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + aResponse.setHeader("Content-Type", "text/plain", false); + aResponse.write(TEST_DATA); + }); + server.start(-1); + + // Open our location asynchronously. + NetUtil.asyncFetch( + { + uri: "http://localhost:" + server.identity.primaryPort + "/test", + loadUsingSystemPrincipal: true, + }, + function (aInputStream, aResult) { + // Check that we had success. + Assert.ok(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + Assert.equal(aInputStream.available(), TEST_DATA.length); + let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + is.init(aInputStream); + let result = is.read(TEST_DATA.length); + Assert.equal(TEST_DATA, result); + + server.stop(run_next_test); + }, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); +} + +function test_asyncFetch_with_nsIFile() { + const TEST_DATA = "this is a test string"; + + // First we need a file to read from. + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("NetUtil-asyncFetch-test-file.tmp"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + // Write the test data to the file. + let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init(file, -1, -1, 0); + ostream.write(TEST_DATA, TEST_DATA.length); + + // Sanity check to make sure the data was written. + Assert.equal(TEST_DATA, getFileContents(file)); + + // Open our file asynchronously. + // Note that this causes main-tread I/O and should be avoided in production. + NetUtil.asyncFetch( + { + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true, + }, + function (aInputStream, aResult) { + // Check that we had success. + Assert.ok(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + Assert.equal(aInputStream.available(), TEST_DATA.length); + let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + is.init(aInputStream); + let result = is.read(TEST_DATA.length); + Assert.equal(TEST_DATA, result); + + run_next_test(); + }, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); +} + +function test_asyncFetch_with_nsIInputString() { + const TEST_DATA = "this is a test string"; + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + istream.setData(TEST_DATA, TEST_DATA.length); + + // Read the input stream asynchronously. + NetUtil.asyncFetch( + istream, + function (aInputStream, aResult) { + // Check that we had success. + Assert.ok(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + Assert.equal(aInputStream.available(), TEST_DATA.length); + Assert.equal( + NetUtil.readInputStreamToString(aInputStream, TEST_DATA.length), + TEST_DATA + ); + + run_next_test(); + }, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); +} + +function test_asyncFetch_does_not_block() { + // Create our channel that has no data. + let channel = NetUtil.newChannel({ + uri: "data:text/plain,", + loadUsingSystemPrincipal: true, + }); + + // Open our channel asynchronously. + NetUtil.asyncFetch(channel, function (aInputStream, aResult) { + // Check that we had success. + Assert.ok(Components.isSuccessCode(aResult)); + + // Check that reading a byte throws that the stream was closed (as opposed + // saying it would block). + let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + is.init(aInputStream); + try { + is.read(1); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_BASE_STREAM_CLOSED); + } + + run_next_test(); + }); +} + +function test_newChannel_no_specifier() { + try { + NetUtil.newChannel(); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_newChannel_with_string() { + const TEST_SPEC = "http://mozilla.org"; + + // Check that we get the same URI back from channel the IO service creates and + // the channel the utility method creates. + let iosChannel = Services.io.newChannel( + TEST_SPEC, + null, + null, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + let NetUtilChannel = NetUtil.newChannel({ + uri: TEST_SPEC, + loadUsingSystemPrincipal: true, + }); + Assert.ok(iosChannel.URI.equals(NetUtilChannel.URI)); + + run_next_test(); +} + +function test_newChannel_with_nsIURI() { + const TEST_SPEC = "http://mozilla.org"; + + // Check that we get the same URI back from channel the IO service creates and + // the channel the utility method creates. + let uri = NetUtil.newURI(TEST_SPEC); + let iosChannel = Services.io.newChannelFromURI( + uri, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + let NetUtilChannel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + Assert.ok(iosChannel.URI.equals(NetUtilChannel.URI)); + + run_next_test(); +} + +function test_newChannel_with_options() { + let uri = "data:text/plain,"; + + let iosChannel = Services.io.newChannelFromURI( + NetUtil.newURI(uri), + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + + function checkEqualToIOSChannel(channel) { + Assert.ok(iosChannel.URI.equals(channel.URI)); + } + + checkEqualToIOSChannel( + NetUtil.newChannel({ + uri, + loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }) + ); + + checkEqualToIOSChannel( + NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }) + ); + + run_next_test(); +} + +function test_newChannel_with_wrong_options() { + let uri = "data:text/plain,"; + let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + + Assert.throws(() => { + NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }, null, null); + }, /requires a single object argument/); + + Assert.throws(() => { + NetUtil.newChannel({ loadUsingSystemPrincipal: true }); + }, /requires the 'uri' property/); + + Assert.throws(() => { + NetUtil.newChannel({ uri, loadingNode: true }); + }, /requires the 'securityFlags'/); + + Assert.throws(() => { + NetUtil.newChannel({ uri, securityFlags: 0 }); + }, /requires at least one of the 'loadingNode'/); + + Assert.throws(() => { + NetUtil.newChannel({ + uri, + loadingPrincipal: systemPrincipal, + securityFlags: 0, + }); + }, /requires the 'contentPolicyType'/); + + Assert.throws(() => { + NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: systemPrincipal, + }); + }, /to be 'true' or 'undefined'/); + + Assert.throws(() => { + NetUtil.newChannel({ + uri, + loadingPrincipal: systemPrincipal, + loadUsingSystemPrincipal: true, + }); + }, /does not accept 'loadUsingSystemPrincipal'/); + + run_next_test(); +} + +function test_readInputStreamToString() { + const TEST_DATA = "this is a test string\0 with an embedded null"; + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsISupportsCString + ); + istream.data = TEST_DATA; + + Assert.equal( + NetUtil.readInputStreamToString(istream, TEST_DATA.length), + TEST_DATA + ); + + run_next_test(); +} + +function test_readInputStreamToString_no_input_stream() { + try { + NetUtil.readInputStreamToString("hi", 2); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_readInputStreamToString_no_bytes_arg() { + const TEST_DATA = "this is a test string"; + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + istream.setData(TEST_DATA, TEST_DATA.length); + + try { + NetUtil.readInputStreamToString(istream); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_readInputStreamToString_blocking_stream() { + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(true, true, 0, 0, null); + + try { + NetUtil.readInputStreamToString(pipe.inputStream, 10); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_BASE_STREAM_WOULD_BLOCK); + } + run_next_test(); +} + +function test_readInputStreamToString_too_many_bytes() { + const TEST_DATA = "this is a test string"; + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + istream.setData(TEST_DATA, TEST_DATA.length); + + try { + NetUtil.readInputStreamToString(istream, TEST_DATA.length + 10); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_FAILURE); + } + + run_next_test(); +} + +function test_readInputStreamToString_with_charset() { + const TEST_DATA = "\uff10\uff11\uff12\uff13"; + const TEST_DATA_UTF8 = "\xef\xbc\x90\xef\xbc\x91\xef\xbc\x92\xef\xbc\x93"; + const TEST_DATA_SJIS = "\x82\x4f\x82\x50\x82\x51\x82\x52"; + + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + + istream.setData(TEST_DATA_UTF8, TEST_DATA_UTF8.length); + Assert.equal( + NetUtil.readInputStreamToString(istream, TEST_DATA_UTF8.length, { + charset: "UTF-8", + }), + TEST_DATA + ); + + istream.setData(TEST_DATA_SJIS, TEST_DATA_SJIS.length); + Assert.equal( + NetUtil.readInputStreamToString(istream, TEST_DATA_SJIS.length, { + charset: "Shift_JIS", + }), + TEST_DATA + ); + + run_next_test(); +} + +function test_readInputStreamToString_invalid_sequence() { + const TEST_DATA = "\ufffd\ufffd\ufffd\ufffd"; + const TEST_DATA_UTF8 = "\xaa\xaa\xaa\xaa"; + + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + + istream.setData(TEST_DATA_UTF8, TEST_DATA_UTF8.length); + try { + NetUtil.readInputStreamToString(istream, TEST_DATA_UTF8.length, { + charset: "UTF-8", + }); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_INPUT); + } + + istream.setData(TEST_DATA_UTF8, TEST_DATA_UTF8.length); + Assert.equal( + NetUtil.readInputStreamToString(istream, TEST_DATA_UTF8.length, { + charset: "UTF-8", + replacement: Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER, + }), + TEST_DATA + ); + + run_next_test(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Test Runner + +[ + test_async_copy, + test_async_write_file, + test_async_write_file_deferred, + test_async_write_file_safe, + test_async_write_file_safe_deferred, + test_newURI_no_spec_throws, + test_newURI, + test_newURI_takes_nsIFile, + test_asyncFetch_no_channel, + test_asyncFetch_no_callback, + test_asyncFetch_with_nsIChannel, + test_asyncFetch_with_nsIURI, + test_asyncFetch_with_string, + test_asyncFetch_with_nsIFile, + test_asyncFetch_with_nsIInputString, + test_asyncFetch_does_not_block, + test_newChannel_no_specifier, + test_newChannel_with_string, + test_newChannel_with_nsIURI, + test_newChannel_with_options, + test_newChannel_with_wrong_options, + test_readInputStreamToString, + test_readInputStreamToString_no_input_stream, + test_readInputStreamToString_no_bytes_arg, + test_readInputStreamToString_blocking_stream, + test_readInputStreamToString_too_many_bytes, + test_readInputStreamToString_with_charset, + test_readInputStreamToString_invalid_sequence, +].forEach(f => add_test(f)); diff --git a/netwerk/test/unit/test_SuperfluousAuth.js b/netwerk/test/unit/test_SuperfluousAuth.js new file mode 100644 index 0000000000..9c28ca54d4 --- /dev/null +++ b/netwerk/test/unit/test_SuperfluousAuth.js @@ -0,0 +1,99 @@ +/* + +Create two http requests with the same URL in which has a user name. We allow +first http request to be loaded and saved in the cache, so the second request +will be served from the cache. However, we disallow loading by returning 1 +in the prompt service. In the end, the second request will be failed. + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://foo@localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +const gMockPromptService = { + firstTimeCalled: false, + confirmExBC() { + if (!this.firstTimeCalled) { + this.firstTimeCalled = true; + return 0; + } + + return 1; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]), +}; + +var gMockPromptServiceCID = MockRegistrar.register( + "@mozilla.org/prompter;1", + gMockPromptService +); + +registerCleanupFunction(() => { + MockRegistrar.unregister(gMockPromptServiceCID); +}); + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +const responseBody = "body"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + response.setHeader("Content-Length", "" + responseBody.length); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + do_get_profile(); + + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function () { + var chan1 = makeChan(URL + "/content"); + chan1.asyncOpen(new ChannelListener(firstTimeThrough, null)); + var chan2 = makeChan(URL + "/content"); + chan2.asyncOpen( + new ChannelListener(secondTimeThrough, null, CL_EXPECT_FAILURE) + ); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + Assert.ok(gMockPromptService.firstTimeCalled, "Prompt service invoked"); +} + +function secondTimeThrough(request, buffer) { + Assert.equal(request.status, Cr.NS_ERROR_ABORT); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_URIs.js b/netwerk/test/unit/test_URIs.js new file mode 100644 index 0000000000..ebbcde52e4 --- /dev/null +++ b/netwerk/test/unit/test_URIs.js @@ -0,0 +1,960 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +// Run by: cd objdir; make -C netwerk/test/ xpcshell-tests +// or: cd objdir; make SOLO_FILE="test_URIs.js" -C netwerk/test/ check-one + +// See also test_URIs2.js. + +// Relevant RFCs: 1738, 1808, 2396, 3986 (newer than the code) +// http://greenbytes.de/tech/webdav/rfc3986.html#rfc.section.5.4 +// http://greenbytes.de/tech/tc/uris/ + +// TEST DATA +// --------- +var gTests = [ + { + spec: "about:blank", + scheme: "about", + prePath: "about:", + pathQueryRef: "blank", + ref: "", + nsIURL: false, + nsINestedURI: true, + immutable: true, + }, + { + spec: "about:foobar", + scheme: "about", + prePath: "about:", + pathQueryRef: "foobar", + ref: "", + nsIURL: false, + nsINestedURI: false, + immutable: true, + }, + { + spec: "chrome://foobar/somedir/somefile.xml", + scheme: "chrome", + prePath: "chrome://foobar", + pathQueryRef: "/somedir/somefile.xml", + ref: "", + nsIURL: true, + nsINestedURI: false, + immutable: true, + }, + { + spec: "data:text/html;charset=utf-8,<html></html>", + scheme: "data", + prePath: "data:", + pathQueryRef: "text/html;charset=utf-8,<html></html>", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "data:text/html;charset=utf-8,<html>\r\n\t</html>", + scheme: "data", + prePath: "data:", + pathQueryRef: "text/html;charset=utf-8,<html></html>", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "data:text/plain,hello%20world", + scheme: "data", + prePath: "data:", + pathQueryRef: "text/plain,hello%20world", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "data:text/plain,hello world", + scheme: "data", + prePath: "data:", + pathQueryRef: "text/plain,hello world", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "file:///dir/afile", + scheme: "data", + prePath: "data:", + pathQueryRef: "text/plain,2", + ref: "", + relativeURI: "data:te\nxt/plain,2", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "file://", + scheme: "file", + prePath: "file://", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "file:///", + scheme: "file", + prePath: "file://", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "file:///myFile.html", + scheme: "file", + prePath: "file://", + pathQueryRef: "/myFile.html", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "file:///dir/afile", + scheme: "file", + prePath: "file://", + pathQueryRef: "/dir/data/text/plain,2", + ref: "", + relativeURI: "data/text/plain,2", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "file:///dir/dir2/", + scheme: "file", + prePath: "file://", + pathQueryRef: "/dir/dir2/data/text/plain,2", + ref: "", + relativeURI: "data/text/plain,2", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "ftp://ftp.mozilla.org/pub/mozilla.org/README", + scheme: "ftp", + prePath: "ftp://ftp.mozilla.org", + pathQueryRef: "/pub/mozilla.org/README", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "ftp://foo:bar@ftp.mozilla.org:100/pub/mozilla.org/README", + scheme: "ftp", + prePath: "ftp://foo:bar@ftp.mozilla.org:100", + port: 100, + username: "foo", + password: "bar", + pathQueryRef: "/pub/mozilla.org/README", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "ftp://foo:@ftp.mozilla.org:100/pub/mozilla.org/README", + scheme: "ftp", + prePath: "ftp://foo@ftp.mozilla.org:100", + port: 100, + username: "foo", + password: "", + pathQueryRef: "/pub/mozilla.org/README", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + //Bug 706249 + { + spec: "gopher://mozilla.org/", + scheme: "gopher", + prePath: "gopher:", + pathQueryRef: "//mozilla.org/", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "http://www.example.com/", + scheme: "http", + prePath: "http://www.example.com", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://www.exa\nmple.com/", + scheme: "http", + prePath: "http://www.example.com", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://10.32.4.239/", + scheme: "http", + prePath: "http://10.32.4.239", + host: "10.32.4.239", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://[::192.9.5.5]/ipng", + scheme: "http", + prePath: "http://[::c009:505]", + host: "::c009:505", + pathQueryRef: "/ipng", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:8888/index.html", + scheme: "http", + prePath: "http://[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:8888", + host: "fedc:ba98:7654:3210:fedc:ba98:7654:3210", + port: 8888, + pathQueryRef: "/index.html", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://bar:foo@www.mozilla.org:8080/pub/mozilla.org/README.html", + scheme: "http", + prePath: "http://bar:foo@www.mozilla.org:8080", + port: 8080, + username: "bar", + password: "foo", + host: "www.mozilla.org", + pathQueryRef: "/pub/mozilla.org/README.html", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "jar:resource://!/", + scheme: "jar", + prePath: "jar:", + pathQueryRef: "resource:///!/", + ref: "", + nsIURL: true, + nsINestedURI: true, + }, + { + spec: "jar:resource://gre/chrome.toolkit.jar!/", + scheme: "jar", + prePath: "jar:", + pathQueryRef: "resource://gre/chrome.toolkit.jar!/", + ref: "", + nsIURL: true, + nsINestedURI: true, + }, + { + spec: "mailto:webmaster@mozilla.com", + scheme: "mailto", + prePath: "mailto:", + pathQueryRef: "webmaster@mozilla.com", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "javascript:new Date()", + scheme: "javascript", + prePath: "javascript:", + pathQueryRef: "new Date()", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "blob:123456", + scheme: "blob", + prePath: "blob:", + pathQueryRef: "123456", + ref: "", + nsIURL: false, + nsINestedURI: false, + immutable: true, + }, + { + spec: "place:sort=8&maxResults=10", + scheme: "place", + prePath: "place:", + pathQueryRef: "sort=8&maxResults=10", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "resource://gre/", + scheme: "resource", + prePath: "resource://gre", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "resource://gre/components/", + scheme: "resource", + prePath: "resource://gre", + pathQueryRef: "/components/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + + // Adding more? Consider adding to test_URIs2.js instead, so that neither + // test runs for *too* long, risking timeouts on slow platforms. +]; + +var gHashSuffixes = ["#", "#myRef", "#myRef?a=b", "#myRef#", "#myRef#x:yz"]; + +// TEST HELPER FUNCTIONS +// --------------------- +function do_info(text, stack) { + if (!stack) { + stack = Components.stack.caller; + } + + dump( + "\n" + + "TEST-INFO | " + + stack.filename + + " | [" + + stack.name + + " : " + + stack.lineNumber + + "] " + + text + + "\n" + ); +} + +// Checks that the URIs satisfy equals(), in both possible orderings. +// Also checks URI.equalsExceptRef(), because equal URIs should also be equal +// when we ignore the ref. +// +// The third argument is optional. If the client passes a third argument +// (e.g. todo_check_true), we'll use that in lieu of ok. +function do_check_uri_eq(aURI1, aURI2, aCheckTrueFunc = ok) { + do_info("(uri equals check: '" + aURI1.spec + "' == '" + aURI2.spec + "')"); + aCheckTrueFunc(aURI1.equals(aURI2)); + do_info("(uri equals check: '" + aURI2.spec + "' == '" + aURI1.spec + "')"); + aCheckTrueFunc(aURI2.equals(aURI1)); + + // (Only take the extra step of testing 'equalsExceptRef' when we expect the + // URIs to really be equal. In 'todo' cases, the URIs may or may not be + // equal when refs are ignored - there's no way of knowing in general.) + if (aCheckTrueFunc == ok) { + do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc); + } +} + +// Checks that the URIs satisfy equalsExceptRef(), in both possible orderings. +// +// The third argument is optional. If the client passes a third argument +// (e.g. todo_check_true), we'll use that in lieu of ok. +function do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc = ok) { + do_info( + "(uri equalsExceptRef check: '" + aURI1.spec + "' == '" + aURI2.spec + "')" + ); + aCheckTrueFunc(aURI1.equalsExceptRef(aURI2)); + do_info( + "(uri equalsExceptRef check: '" + aURI2.spec + "' == '" + aURI1.spec + "')" + ); + aCheckTrueFunc(aURI2.equalsExceptRef(aURI1)); +} + +// Checks that the given property on aURI matches the corresponding property +// in the test bundle (or matches some function of that corresponding property, +// if aTestFunctor is passed in). +function do_check_property(aTest, aURI, aPropertyName, aTestFunctor) { + if (aTest[aPropertyName]) { + var expectedVal = aTestFunctor + ? aTestFunctor(aTest[aPropertyName]) + : aTest[aPropertyName]; + + do_info( + "testing " + + aPropertyName + + " of " + + (aTestFunctor ? "modified '" : "'") + + aTest.spec + + "' is '" + + expectedVal + + "'" + ); + Assert.equal(aURI[aPropertyName], expectedVal); + } +} + +// Test that a given URI parses correctly into its various components. +function do_test_uri_basic(aTest) { + var URI; + + do_info( + "Basic tests for " + + aTest.spec + + " relative URI: " + + (aTest.relativeURI === undefined ? "(none)" : aTest.relativeURI) + ); + + try { + URI = NetUtil.newURI(aTest.spec); + } catch (e) { + do_info("Caught error on parse of" + aTest.spec + " Error: " + e.result); + if (aTest.fail) { + Assert.equal(e.result, aTest.result); + return; + } + do_throw(e.result); + } + + if (aTest.relativeURI) { + var relURI; + + try { + relURI = Services.io.newURI(aTest.relativeURI, null, URI); + } catch (e) { + do_info( + "Caught error on Relative parse of " + + aTest.spec + + " + " + + aTest.relativeURI + + " Error: " + + e.result + ); + if (aTest.relativeFail) { + Assert.equal(e.result, aTest.relativeFail); + return; + } + do_throw(e.result); + } + do_info( + "relURI.pathQueryRef = " + + relURI.pathQueryRef + + ", was " + + URI.pathQueryRef + ); + URI = relURI; + do_info("URI.pathQueryRef now = " + URI.pathQueryRef); + } + + // Sanity-check + do_info("testing " + aTest.spec + " equals a clone of itself"); + do_check_uri_eq(URI, URI.mutate().finalize()); + do_check_uri_eqExceptRef(URI, URI.mutate().setRef("").finalize()); + do_info("testing " + aTest.spec + " instanceof nsIURL"); + Assert.equal(URI instanceof Ci.nsIURL, aTest.nsIURL); + do_info("testing " + aTest.spec + " instanceof nsINestedURI"); + Assert.equal(URI instanceof Ci.nsINestedURI, aTest.nsINestedURI); + + do_info( + "testing that " + + aTest.spec + + " throws or returns false " + + "from equals(null)" + ); + // XXXdholbert At some point it'd probably be worth making this behavior + // (throwing vs. returning false) consistent across URI implementations. + var threw = false; + var isEqualToNull; + try { + isEqualToNull = URI.equals(null); + } catch (e) { + threw = true; + } + Assert.ok(threw || !isEqualToNull); + + // Check the various components + do_check_property(aTest, URI, "scheme"); + do_check_property(aTest, URI, "prePath"); + do_check_property(aTest, URI, "pathQueryRef"); + do_check_property(aTest, URI, "query"); + do_check_property(aTest, URI, "ref"); + do_check_property(aTest, URI, "port"); + do_check_property(aTest, URI, "username"); + do_check_property(aTest, URI, "password"); + do_check_property(aTest, URI, "host"); + do_check_property(aTest, URI, "specIgnoringRef"); + if ("hasRef" in aTest) { + do_info("testing hasref: " + aTest.hasRef + " vs " + URI.hasRef); + Assert.equal(aTest.hasRef, URI.hasRef); + } +} + +// Test that a given URI parses correctly when we add a given ref to the end +function do_test_uri_with_hash_suffix(aTest, aSuffix) { + do_info("making sure caller is using suffix that starts with '#'"); + Assert.equal(aSuffix[0], "#"); + + var origURI = NetUtil.newURI(aTest.spec); + var testURI; + + if (aTest.relativeURI) { + try { + origURI = Services.io.newURI(aTest.relativeURI, null, origURI); + } catch (e) { + do_info( + "Caught error on Relative parse of " + + aTest.spec + + " + " + + aTest.relativeURI + + " Error: " + + e.result + ); + return; + } + try { + testURI = Services.io.newURI(aSuffix, null, origURI); + } catch (e) { + do_info( + "Caught error adding suffix to " + + aTest.spec + + " + " + + aTest.relativeURI + + ", suffix " + + aSuffix + + " Error: " + + e.result + ); + return; + } + } else { + testURI = NetUtil.newURI(aTest.spec + aSuffix); + } + + do_info( + "testing " + + aTest.spec + + " with '" + + aSuffix + + "' appended " + + "equals a clone of itself" + ); + do_check_uri_eq(testURI, testURI.mutate().finalize()); + + do_info( + "testing " + + aTest.spec + + " doesn't equal self with '" + + aSuffix + + "' appended" + ); + + Assert.ok(!origURI.equals(testURI)); + + do_info( + "testing " + + aTest.spec + + " is equalExceptRef to self with '" + + aSuffix + + "' appended" + ); + do_check_uri_eqExceptRef(origURI, testURI); + + Assert.equal(testURI.hasRef, true); + + if (!origURI.ref) { + // These tests fail if origURI has a ref + do_info( + "testing setRef('') on " + + testURI.spec + + " is equal to no-ref version but not equal to ref version" + ); + var cloneNoRef = testURI.mutate().setRef("").finalize(); // we used to clone here. + do_info("cloneNoRef: " + cloneNoRef.spec + " hasRef: " + cloneNoRef.hasRef); + do_info("testURI: " + testURI.spec + " hasRef: " + testURI.hasRef); + do_check_uri_eq(cloneNoRef, origURI); + Assert.ok(!cloneNoRef.equals(testURI)); + + do_info( + "testing cloneWithNewRef on " + + testURI.spec + + " with an empty ref is equal to no-ref version but not equal to ref version" + ); + var cloneNewRef = testURI.mutate().setRef("").finalize(); + do_check_uri_eq(cloneNewRef, origURI); + do_check_uri_eq(cloneNewRef, cloneNoRef); + Assert.ok(!cloneNewRef.equals(testURI)); + + do_info( + "testing cloneWithNewRef on " + + origURI.spec + + " with the same new ref is equal to ref version and not equal to no-ref version" + ); + cloneNewRef = origURI.mutate().setRef(aSuffix).finalize(); + do_check_uri_eq(cloneNewRef, testURI); + Assert.ok(cloneNewRef.equals(testURI)); + } + + do_check_property(aTest, testURI, "scheme"); + do_check_property(aTest, testURI, "prePath"); + if (!origURI.ref) { + // These don't work if it's a ref already because '+' doesn't give the right result + do_check_property(aTest, testURI, "pathQueryRef", function (aStr) { + return aStr + aSuffix; + }); + do_check_property(aTest, testURI, "ref", function (aStr) { + return aSuffix.substr(1); + }); + } +} + +// Tests various ways of setting & clearing a ref on a URI. +function do_test_mutate_ref(aTest, aSuffix) { + do_info("making sure caller is using suffix that starts with '#'"); + Assert.equal(aSuffix[0], "#"); + + var refURIWithSuffix = NetUtil.newURI(aTest.spec + aSuffix); + var refURIWithoutSuffix = NetUtil.newURI(aTest.spec); + + var testURI = NetUtil.newURI(aTest.spec); + + // First: Try setting .ref to our suffix + do_info( + "testing that setting .ref on " + + aTest.spec + + " to '" + + aSuffix + + "' does what we expect" + ); + testURI = testURI.mutate().setRef(aSuffix).finalize(); + do_check_uri_eq(testURI, refURIWithSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithoutSuffix); + + // Now try setting .ref but leave off the initial hash (expect same result) + var suffixLackingHash = aSuffix.substr(1); + if (suffixLackingHash) { + // (skip this our suffix was *just* a #) + do_info( + "testing that setting .ref on " + + aTest.spec + + " to '" + + suffixLackingHash + + "' does what we expect" + ); + testURI = testURI.mutate().setRef(suffixLackingHash).finalize(); + do_check_uri_eq(testURI, refURIWithSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithoutSuffix); + } + + // Now, clear .ref (should get us back the original spec) + do_info( + "testing that clearing .ref on " + testURI.spec + " does what we expect" + ); + testURI = testURI.mutate().setRef("").finalize(); + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + if (!aTest.relativeURI) { + // TODO: These tests don't work as-is for relative URIs. + + // Now try setting .spec directly (including suffix) and then clearing .ref + var specWithSuffix = aTest.spec + aSuffix; + do_info( + "testing that setting spec to " + + specWithSuffix + + " and then clearing ref does what we expect" + ); + + testURI = testURI.mutate().setSpec(specWithSuffix).setRef("").finalize(); + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + // XXX nsIJARURI throws an exception in SetPath(), so skip it for next part. + if (!(testURI instanceof Ci.nsIJARURI)) { + // Now try setting .pathQueryRef directly (including suffix) and then clearing .ref + // (same as above, but with now with .pathQueryRef instead of .spec) + testURI = NetUtil.newURI(aTest.spec); + + var pathWithSuffix = aTest.pathQueryRef + aSuffix; + do_info( + "testing that setting path to " + + pathWithSuffix + + " and then clearing ref does what we expect" + ); + testURI = testURI + .mutate() + .setPathQueryRef(pathWithSuffix) + .setRef("") + .finalize(); + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + // Also: make sure that clearing .pathQueryRef also clears .ref + testURI = testURI.mutate().setPathQueryRef(pathWithSuffix).finalize(); + do_info( + "testing that clearing path from " + + pathWithSuffix + + " also clears .ref" + ); + testURI = testURI.mutate().setPathQueryRef("").finalize(); + Assert.equal(testURI.ref, ""); + } + } +} + +// Check that changing nested/about URIs works correctly. +add_task(function check_nested_mutations() { + // nsNestedAboutURI + let uri1 = Services.io.newURI("about:blank#"); + let uri2 = Services.io.newURI("about:blank"); + let uri3 = uri1.mutate().setRef("").finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2.mutate().setRef("#").finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = Services.io.newURI("about:blank?something"); + uri2 = Services.io.newURI("about:blank"); + uri3 = uri1.mutate().setQuery("").finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2.mutate().setQuery("something").finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = Services.io.newURI("about:blank?query#ref"); + uri2 = Services.io.newURI("about:blank"); + uri3 = uri1.mutate().setPathQueryRef("blank").finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2.mutate().setPathQueryRef("blank?query#ref").finalize(); + do_check_uri_eq(uri3, uri1); + + // nsSimpleNestedURI + uri1 = Services.io.newURI("view-source:http://example.com/path#"); + uri2 = Services.io.newURI("view-source:http://example.com/path"); + uri3 = uri1.mutate().setRef("").finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2.mutate().setRef("#").finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = Services.io.newURI("view-source:http://example.com/path?something"); + uri2 = Services.io.newURI("view-source:http://example.com/path"); + uri3 = uri1.mutate().setQuery("").finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2.mutate().setQuery("something").finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = Services.io.newURI("view-source:http://example.com/path?query#ref"); + uri2 = Services.io.newURI("view-source:http://example.com/path"); + uri3 = uri1.mutate().setPathQueryRef("path").finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2.mutate().setPathQueryRef("path?query#ref").finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = Services.io.newURI("view-source:about:blank#"); + uri2 = Services.io.newURI("view-source:about:blank"); + uri3 = uri1.mutate().setRef("").finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2.mutate().setRef("#").finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = Services.io.newURI("view-source:about:blank?something"); + uri2 = Services.io.newURI("view-source:about:blank"); + uri3 = uri1.mutate().setQuery("").finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2.mutate().setQuery("something").finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = Services.io.newURI("view-source:about:blank?query#ref"); + uri2 = Services.io.newURI("view-source:about:blank"); + uri3 = uri1.mutate().setPathQueryRef("blank").finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2.mutate().setPathQueryRef("blank?query#ref").finalize(); + do_check_uri_eq(uri3, uri1); +}); + +add_task(function check_space_escaping() { + let uri = Services.io.newURI("data:text/plain,hello%20world#space hash"); + Assert.equal(uri.spec, "data:text/plain,hello%20world#space%20hash"); + uri = Services.io.newURI("data:text/plain,hello%20world#space%20hash"); + Assert.equal(uri.spec, "data:text/plain,hello%20world#space%20hash"); + uri = Services.io.newURI("data:text/plain,hello world#space%20hash"); + Assert.equal(uri.spec, "data:text/plain,hello world#space%20hash"); + uri = Services.io.newURI("data:text/plain,hello world#space hash"); + Assert.equal(uri.spec, "data:text/plain,hello world#space%20hash"); + uri = Services.io.newURI("http://example.com/test path#test path"); + uri = Services.io.newURI("http://example.com/test%20path#test%20path"); +}); + +add_task(function check_schemeIsNull() { + let uri = Services.io.newURI("data:text/plain,aaa"); + Assert.ok(!uri.schemeIs(null)); + uri = Services.io.newURI("http://example.com"); + Assert.ok(!uri.schemeIs(null)); + uri = Services.io.newURI("dummyscheme://example.com"); + Assert.ok(!uri.schemeIs(null)); + uri = Services.io.newURI("jar:resource://gre/chrome.toolkit.jar!/"); + Assert.ok(!uri.schemeIs(null)); + uri = Services.io.newURI("moz-icon://.unknown?size=32"); + Assert.ok(!uri.schemeIs(null)); +}); + +// Check that characters in the query of moz-extension aren't improperly unescaped (Bug 1547882) +add_task(function check_mozextension_query() { + let uri = Services.io.newURI( + "moz-extension://a7d1572e-3beb-4d93-a920-c408fa09e8ea/_source/holding.html" + ); + uri = uri + .mutate() + .setQuery("u=https%3A%2F%2Fnews.ycombinator.com%2F") + .finalize(); + Assert.equal(uri.query, "u=https%3A%2F%2Fnews.ycombinator.com%2F"); + uri = Services.io.newURI( + "moz-extension://a7d1572e-3beb-4d93-a920-c408fa09e8ea/_source/holding.html?u=https%3A%2F%2Fnews.ycombinator.com%2F" + ); + Assert.equal( + uri.spec, + "moz-extension://a7d1572e-3beb-4d93-a920-c408fa09e8ea/_source/holding.html?u=https%3A%2F%2Fnews.ycombinator.com%2F" + ); + Assert.equal(uri.query, "u=https%3A%2F%2Fnews.ycombinator.com%2F"); +}); + +add_task(function check_resolve() { + let base = Services.io.newURI("http://example.com"); + let uri = Services.io.newURI("tel::+371 27028456", "utf-8", base); + Assert.equal(uri.spec, "tel::+371 27028456"); +}); + +add_task(function test_extra_protocols() { + // dweb:// + let url = Services.io.newURI("dweb://example.com/test"); + Assert.equal(url.host, "example.com"); + + // dat:// + url = Services.io.newURI( + "dat://41f8a987cfeba80a037e51cc8357d513b62514de36f2f9b3d3eeec7a8fb3b5a5/" + ); + Assert.equal( + url.host, + "41f8a987cfeba80a037e51cc8357d513b62514de36f2f9b3d3eeec7a8fb3b5a5" + ); + url = Services.io.newURI("dat://example.com/test"); + Assert.equal(url.host, "example.com"); + + // ipfs:// + url = Services.io.newURI( + "ipfs://bafybeiccfclkdtucu6y4yc5cpr6y3yuinr67svmii46v5cfcrkp47ihehy/frontend/license.txt" + ); + Assert.equal(url.scheme, "ipfs"); + Assert.equal( + url.host, + "bafybeiccfclkdtucu6y4yc5cpr6y3yuinr67svmii46v5cfcrkp47ihehy" + ); + Assert.equal(url.filePath, "/frontend/license.txt"); + + // ipns:// + url = Services.io.newURI("ipns://peerdium.gozala.io/index.html"); + Assert.equal(url.scheme, "ipns"); + Assert.equal(url.host, "peerdium.gozala.io"); + Assert.equal(url.filePath, "/index.html"); + + // ssb:// + url = Services.io.newURI("ssb://scuttlebutt.nz/index.html"); + Assert.equal(url.scheme, "ssb"); + Assert.equal(url.host, "scuttlebutt.nz"); + Assert.equal(url.filePath, "/index.html"); + + // wtp:// + url = Services.io.newURI( + "wtp://951ead31d09e4049fc1f21f137e233dd0589fcbd/blog/vim-tips/" + ); + Assert.equal(url.scheme, "wtp"); + Assert.equal(url.host, "951ead31d09e4049fc1f21f137e233dd0589fcbd"); + Assert.equal(url.filePath, "/blog/vim-tips/"); +}); + +// TEST MAIN FUNCTION +// ------------------ +add_task(function mainTest() { + // UTF-8 check - From bug 622981 + // ASCII + let base = Services.io.newURI("http://example.org/xenia?"); + let resolved = Services.io.newURI("?x", null, base); + let expected = Services.io.newURI("http://example.org/xenia?x"); + do_info( + "Bug 662981: ACSII - comparing " + resolved.spec + " and " + expected.spec + ); + Assert.ok(resolved.equals(expected)); + + // UTF-8 character "è" + // Bug 622981 was triggered by an empty query string + base = Services.io.newURI("http://example.org/xènia?"); + resolved = Services.io.newURI("?x", null, base); + expected = Services.io.newURI("http://example.org/xènia?x"); + do_info( + "Bug 662981: UTF8 - comparing " + resolved.spec + " and " + expected.spec + ); + Assert.ok(resolved.equals(expected)); + + gTests.forEach(function (aTest) { + // Check basic URI functionality + do_test_uri_basic(aTest); + + if (!aTest.fail) { + // Try adding various #-prefixed strings to the ends of the URIs + gHashSuffixes.forEach(function (aSuffix) { + do_test_uri_with_hash_suffix(aTest, aSuffix); + if (!aTest.immutable) { + do_test_mutate_ref(aTest, aSuffix); + } + }); + + // For URIs that we couldn't mutate above due to them being immutable: + // Now we check that they're actually immutable. + if (aTest.immutable) { + Assert.ok(aTest.immutable); + } + } + }); +}); + +function check_round_trip_serialization(spec) { + dump(`checking ${spec}\n`); + let uri = Services.io.newURI(spec); + let str = serialize_to_escaped_string(uri); + let other = deserialize_from_escaped_string(str).QueryInterface(Ci.nsIURI); + equal(other.spec, uri.spec); +} + +add_task(function test_iconURI_serialization() { + // URIs taken from test_moz_icon_uri.js + + let tests = [ + "moz-icon://foo.html?contentType=bar&size=button&state=normal", + "moz-icon://foo.html?size=3", + "moz-icon://stock/foo", + "moz-icon:file://foo.txt", + "moz-icon://file://foo.txt", + ]; + + tests.forEach(str => check_round_trip_serialization(str)); +}); + +add_task(function test_jarURI_serialization() { + check_round_trip_serialization("jar:http://example.com/bar.jar!/"); +}); + +add_task(async function round_trip_invalid_ace_label() { + let uri = Services.io.newURI("http://xn--xn--d--fg4n-5y45d/"); + Assert.equal(uri.spec, "http://xn--xn--d--fg4n-5y45d/"); + + Assert.throws(() => { + uri = Services.io.newURI("http://a.b.c.XN--pokxncvks"); + }, /NS_ERROR_MALFORMED_URI/); +}); diff --git a/netwerk/test/unit/test_URIs2.js b/netwerk/test/unit/test_URIs2.js new file mode 100644 index 0000000000..75e88c595a --- /dev/null +++ b/netwerk/test/unit/test_URIs2.js @@ -0,0 +1,875 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +"use strict"; + +// Run by: cd objdir; make -C netwerk/test/ xpcshell-tests +// or: cd objdir; make SOLO_FILE="test_URIs2.js" -C netwerk/test/ check-one + +// This is a clone of test_URIs.js, with a different set of test data in gTests. +// The original test data in test_URIs.js was split between test_URIs and test_URIs2.js +// because test_URIs.js was running for too long on slow platforms, causing +// intermittent timeouts. + +// Relevant RFCs: 1738, 1808, 2396, 3986 (newer than the code) +// http://greenbytes.de/tech/webdav/rfc3986.html#rfc.section.5.4 +// http://greenbytes.de/tech/tc/uris/ + +// TEST DATA +// --------- +var gTests = [ + { + spec: "view-source:about:blank", + scheme: "view-source", + prePath: "view-source:", + pathQueryRef: "about:blank", + ref: "", + nsIURL: false, + nsINestedURI: true, + immutable: true, + }, + { + spec: "view-source:http://www.mozilla.org/", + scheme: "view-source", + prePath: "view-source:", + pathQueryRef: "http://www.mozilla.org/", + ref: "", + nsIURL: false, + nsINestedURI: true, + immutable: true, + }, + { + spec: "x-external:", + scheme: "x-external", + prePath: "x-external:", + pathQueryRef: "", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "x-external:abc", + scheme: "x-external", + prePath: "x-external:", + pathQueryRef: "abc", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "http://www2.example.com/", + relativeURI: "a/b/c/d", + scheme: "http", + prePath: "http://www2.example.com", + pathQueryRef: "/a/b/c/d", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + // relative URL testcases from http://greenbytes.de/tech/webdav/rfc3986.html#rfc.section.5.4 + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g:h", + scheme: "g", + prePath: "g:", + pathQueryRef: "h", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "./g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g/", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "/g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "?y", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/d;p?y", + ref: "", // fix + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g?y", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g?y", + ref: "", // fix + specIgnoringRef: "http://a/b/c/g?y", + hasRef: false, + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "#s", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/d;p?q#s", + ref: "s", // fix + specIgnoringRef: "http://a/b/c/d;p?q", + hasRef: true, + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g#s", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g#s", + ref: "s", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g?y#s", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g?y#s", + ref: "s", + nsIURL: true, + nsINestedURI: false, + }, + /* + Bug xxxxxx - we return a path of b/c/;x + { spec: "http://a/b/c/d;p?q", + relativeURI: ";x", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/d;x", + ref: "", + nsIURL: true, nsINestedURI: false }, + */ + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g;x", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g;x", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g;x?y#s", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g;x?y#s", + ref: "s", + nsIURL: true, + nsINestedURI: false, + }, + /* + Can't easily specify a relative URI of "" to the test code + { spec: "http://a/b/c/d;p?q", + relativeURI: "", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/d", + ref: "", + nsIURL: true, nsINestedURI: false }, + */ + { + spec: "http://a/b/c/d;p?q", + relativeURI: ".", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "./", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "..", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../..", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../../", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../../g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + + // abnormal examples + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../../../g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../../../../g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + + // coalesce + { + spec: "http://a/b/c/d;p?q", + relativeURI: "/./g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "/../g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g.", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g.", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: ".g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/.g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g..", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g..", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "..g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/..g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: ".", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "./../g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "./g/.", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g/./h", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g/h", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g/../h", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/h", + ref: "", // fix + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g;x=1/./y", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g;x=1/y", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g;x=1/../y", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/y", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + // protocol-relative http://tools.ietf.org/html/rfc3986#section-4.2 + { + spec: "http://www2.example.com/", + relativeURI: "//www3.example2.com/bar", + scheme: "http", + prePath: "http://www3.example2.com", + pathQueryRef: "/bar", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "https://www2.example.com/", + relativeURI: "//www3.example2.com/bar", + scheme: "https", + prePath: "https://www3.example2.com", + pathQueryRef: "/bar", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, +]; + +var gHashSuffixes = ["#", "#myRef", "#myRef?a=b", "#myRef#", "#myRef#x:yz"]; + +// TEST HELPER FUNCTIONS +// --------------------- +function do_info(text, stack) { + if (!stack) { + stack = Components.stack.caller; + } + + dump( + "\n" + + "TEST-INFO | " + + stack.filename + + " | [" + + stack.name + + " : " + + stack.lineNumber + + "] " + + text + + "\n" + ); +} + +// Checks that the URIs satisfy equals(), in both possible orderings. +// Also checks URI.equalsExceptRef(), because equal URIs should also be equal +// when we ignore the ref. +// +// The third argument is optional. If the client passes a third argument +// (e.g. todo_check_true), we'll use that in lieu of ok. +function do_check_uri_eq(aURI1, aURI2, aCheckTrueFunc = ok) { + do_info("(uri equals check: '" + aURI1.spec + "' == '" + aURI2.spec + "')"); + aCheckTrueFunc(aURI1.equals(aURI2)); + do_info("(uri equals check: '" + aURI2.spec + "' == '" + aURI1.spec + "')"); + aCheckTrueFunc(aURI2.equals(aURI1)); + + // (Only take the extra step of testing 'equalsExceptRef' when we expect the + // URIs to really be equal. In 'todo' cases, the URIs may or may not be + // equal when refs are ignored - there's no way of knowing in general.) + if (aCheckTrueFunc == ok) { + do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc); + } +} + +// Checks that the URIs satisfy equalsExceptRef(), in both possible orderings. +// +// The third argument is optional. If the client passes a third argument +// (e.g. todo_check_true), we'll use that in lieu of ok. +function do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc = ok) { + do_info( + "(uri equalsExceptRef check: '" + aURI1.spec + "' == '" + aURI2.spec + "')" + ); + aCheckTrueFunc(aURI1.equalsExceptRef(aURI2)); + do_info( + "(uri equalsExceptRef check: '" + aURI2.spec + "' == '" + aURI1.spec + "')" + ); + aCheckTrueFunc(aURI2.equalsExceptRef(aURI1)); +} + +// Checks that the given property on aURI matches the corresponding property +// in the test bundle (or matches some function of that corresponding property, +// if aTestFunctor is passed in). +function do_check_property(aTest, aURI, aPropertyName, aTestFunctor) { + if (aTest[aPropertyName]) { + var expectedVal = aTestFunctor + ? aTestFunctor(aTest[aPropertyName]) + : aTest[aPropertyName]; + + do_info( + "testing " + + aPropertyName + + " of " + + (aTestFunctor ? "modified '" : "'") + + aTest.spec + + "' is '" + + expectedVal + + "'" + ); + Assert.equal(aURI[aPropertyName], expectedVal); + } +} + +// Test that a given URI parses correctly into its various components. +function do_test_uri_basic(aTest) { + var URI; + + do_info( + "Basic tests for " + aTest.spec + " relative URI: " + aTest.relativeURI + ); + + try { + URI = NetUtil.newURI(aTest.spec); + } catch (e) { + do_info("Caught error on parse of" + aTest.spec + " Error: " + e.result); + if (aTest.fail) { + Assert.equal(e.result, aTest.result); + return; + } + do_throw(e.result); + } + + if (aTest.relativeURI) { + var relURI; + + try { + relURI = Services.io.newURI(aTest.relativeURI, null, URI); + } catch (e) { + do_info( + "Caught error on Relative parse of " + + aTest.spec + + " + " + + aTest.relativeURI + + " Error: " + + e.result + ); + if (aTest.relativeFail) { + Assert.equal(e.result, aTest.relativeFail); + return; + } + do_throw(e.result); + } + do_info( + "relURI.pathQueryRef = " + + relURI.pathQueryRef + + ", was " + + URI.pathQueryRef + ); + URI = relURI; + do_info("URI.pathQueryRef now = " + URI.pathQueryRef); + } + + // Sanity-check + do_info("testing " + aTest.spec + " equals a clone of itself"); + do_check_uri_eq(URI, URI.mutate().finalize()); + do_check_uri_eqExceptRef(URI, URI.mutate().setRef("").finalize()); + do_info("testing " + aTest.spec + " instanceof nsIURL"); + Assert.equal(URI instanceof Ci.nsIURL, aTest.nsIURL); + do_info("testing " + aTest.spec + " instanceof nsINestedURI"); + Assert.equal(URI instanceof Ci.nsINestedURI, aTest.nsINestedURI); + + do_info( + "testing that " + + aTest.spec + + " throws or returns false " + + "from equals(null)" + ); + // XXXdholbert At some point it'd probably be worth making this behavior + // (throwing vs. returning false) consistent across URI implementations. + var threw = false; + var isEqualToNull; + try { + isEqualToNull = URI.equals(null); + } catch (e) { + threw = true; + } + Assert.ok(threw || !isEqualToNull); + + // Check the various components + do_check_property(aTest, URI, "scheme"); + do_check_property(aTest, URI, "prePath"); + do_check_property(aTest, URI, "pathQueryRef"); + do_check_property(aTest, URI, "query"); + do_check_property(aTest, URI, "ref"); + do_check_property(aTest, URI, "port"); + do_check_property(aTest, URI, "username"); + do_check_property(aTest, URI, "password"); + do_check_property(aTest, URI, "host"); + do_check_property(aTest, URI, "specIgnoringRef"); + if ("hasRef" in aTest) { + do_info("testing hasref: " + aTest.hasRef + " vs " + URI.hasRef); + Assert.equal(aTest.hasRef, URI.hasRef); + } +} + +// Test that a given URI parses correctly when we add a given ref to the end +function do_test_uri_with_hash_suffix(aTest, aSuffix) { + do_info("making sure caller is using suffix that starts with '#'"); + Assert.equal(aSuffix[0], "#"); + + var origURI = NetUtil.newURI(aTest.spec); + var testURI; + + if (aTest.relativeURI) { + try { + origURI = Services.io.newURI(aTest.relativeURI, null, origURI); + } catch (e) { + do_info( + "Caught error on Relative parse of " + + aTest.spec + + " + " + + aTest.relativeURI + + " Error: " + + e.result + ); + return; + } + try { + testURI = Services.io.newURI(aSuffix, null, origURI); + } catch (e) { + do_info( + "Caught error adding suffix to " + + aTest.spec + + " + " + + aTest.relativeURI + + ", suffix " + + aSuffix + + " Error: " + + e.result + ); + return; + } + } else { + testURI = NetUtil.newURI(aTest.spec + aSuffix); + } + + do_info( + "testing " + + aTest.spec + + " with '" + + aSuffix + + "' appended " + + "equals a clone of itself" + ); + do_check_uri_eq(testURI, testURI.mutate().finalize()); + + do_info( + "testing " + + aTest.spec + + " doesn't equal self with '" + + aSuffix + + "' appended" + ); + + Assert.ok(!origURI.equals(testURI)); + + do_info( + "testing " + + aTest.spec + + " is equalExceptRef to self with '" + + aSuffix + + "' appended" + ); + do_check_uri_eqExceptRef(origURI, testURI); + + Assert.equal(testURI.hasRef, true); + + if (!origURI.ref) { + // These tests fail if origURI has a ref + do_info( + "testing cloneIgnoringRef on " + + testURI.spec + + " is equal to no-ref version but not equal to ref version" + ); + var cloneNoRef = testURI.mutate().setRef("").finalize(); + do_check_uri_eq(cloneNoRef, origURI); + Assert.ok(!cloneNoRef.equals(testURI)); + } + + do_check_property(aTest, testURI, "scheme"); + do_check_property(aTest, testURI, "prePath"); + if (!origURI.ref) { + // These don't work if it's a ref already because '+' doesn't give the right result + do_check_property(aTest, testURI, "pathQueryRef", function (aStr) { + return aStr + aSuffix; + }); + do_check_property(aTest, testURI, "ref", function (aStr) { + return aSuffix.substr(1); + }); + } +} + +// Tests various ways of setting & clearing a ref on a URI. +function do_test_mutate_ref(aTest, aSuffix) { + do_info("making sure caller is using suffix that starts with '#'"); + Assert.equal(aSuffix[0], "#"); + + var refURIWithSuffix = NetUtil.newURI(aTest.spec + aSuffix); + var refURIWithoutSuffix = NetUtil.newURI(aTest.spec); + + var testURI = NetUtil.newURI(aTest.spec); + + // First: Try setting .ref to our suffix + do_info( + "testing that setting .ref on " + + aTest.spec + + " to '" + + aSuffix + + "' does what we expect" + ); + testURI = testURI.mutate().setRef(aSuffix).finalize(); + do_check_uri_eq(testURI, refURIWithSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithoutSuffix); + + // Now try setting .ref but leave off the initial hash (expect same result) + var suffixLackingHash = aSuffix.substr(1); + if (suffixLackingHash) { + // (skip this our suffix was *just* a #) + do_info( + "testing that setting .ref on " + + aTest.spec + + " to '" + + suffixLackingHash + + "' does what we expect" + ); + testURI = testURI.mutate().setRef(suffixLackingHash).finalize(); + do_check_uri_eq(testURI, refURIWithSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithoutSuffix); + } + + // Now, clear .ref (should get us back the original spec) + do_info( + "testing that clearing .ref on " + testURI.spec + " does what we expect" + ); + testURI = testURI.mutate().setRef("").finalize(); + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + if (!aTest.relativeURI) { + // TODO: These tests don't work as-is for relative URIs. + + // Now try setting .spec directly (including suffix) and then clearing .ref + var specWithSuffix = aTest.spec + aSuffix; + do_info( + "testing that setting spec to " + + specWithSuffix + + " and then clearing ref does what we expect" + ); + testURI = testURI.mutate().setSpec(specWithSuffix).setRef("").finalize(); + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + // XXX nsIJARURI throws an exception in SetPath(), so skip it for next part. + if (!(testURI instanceof Ci.nsIJARURI)) { + // Now try setting .pathQueryRef directly (including suffix) and then clearing .ref + // (same as above, but with now with .pathQueryRef instead of .spec) + testURI = NetUtil.newURI(aTest.spec); + + var pathWithSuffix = aTest.pathQueryRef + aSuffix; + do_info( + "testing that setting path to " + + pathWithSuffix + + " and then clearing ref does what we expect" + ); + testURI = testURI + .mutate() + .setPathQueryRef(pathWithSuffix) + .setRef("") + .finalize(); + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + // Also: make sure that clearing .pathQueryRef also clears .ref + testURI = testURI.mutate().setPathQueryRef(pathWithSuffix).finalize(); + do_info( + "testing that clearing path from " + + pathWithSuffix + + " also clears .ref" + ); + testURI = testURI.mutate().setPathQueryRef("").finalize(); + Assert.equal(testURI.ref, ""); + } + } +} + +// TEST MAIN FUNCTION +// ------------------ +function run_test() { + // UTF-8 check - From bug 622981 + // ASCII + let base = Services.io.newURI("http://example.org/xenia?"); + let resolved = Services.io.newURI("?x", null, base); + let expected = Services.io.newURI("http://example.org/xenia?x"); + do_info( + "Bug 662981: ACSII - comparing " + resolved.spec + " and " + expected.spec + ); + Assert.ok(resolved.equals(expected)); + + // UTF-8 character "è" + // Bug 622981 was triggered by an empty query string + base = Services.io.newURI("http://example.org/xènia?"); + resolved = Services.io.newURI("?x", null, base); + expected = Services.io.newURI("http://example.org/xènia?x"); + do_info( + "Bug 662981: UTF8 - comparing " + resolved.spec + " and " + expected.spec + ); + Assert.ok(resolved.equals(expected)); + + gTests.forEach(function (aTest) { + // Check basic URI functionality + do_test_uri_basic(aTest); + + if (!aTest.fail) { + // Try adding various #-prefixed strings to the ends of the URIs + gHashSuffixes.forEach(function (aSuffix) { + do_test_uri_with_hash_suffix(aTest, aSuffix); + if (!aTest.immutable) { + do_test_mutate_ref(aTest, aSuffix); + } + }); + + // For URIs that we couldn't mutate above due to them being immutable: + // Now we check that they're actually immutable. + if (aTest.immutable) { + Assert.ok(aTest.immutable); + } + } + }); +} diff --git a/netwerk/test/unit/test_XHR_redirects.js b/netwerk/test/unit/test_XHR_redirects.js new file mode 100644 index 0000000000..3edd6a02c2 --- /dev/null +++ b/netwerk/test/unit/test_XHR_redirects.js @@ -0,0 +1,273 @@ +// This file tests whether XmlHttpRequests correctly handle redirects, +// including rewriting POSTs to GETs (on 301/302/303), as well as +// prompting for redirects of other unsafe methods (such as PUTs, DELETEs, +// etc--see HttpBaseChannel::IsSafeMethod). Since no prompting is possible +// in xpcshell, we get an error for prompts, and the request fails. +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); + +var sSame; +var sOther; +var sRedirectPromptPref; + +const BUGID = "676059"; +const OTHERBUGID = "696849"; + +XPCOMUtils.defineLazyGetter(this, "pSame", function () { + return sSame.identity.primaryPort; +}); +XPCOMUtils.defineLazyGetter(this, "pOther", function () { + return sOther.identity.primaryPort; +}); + +function createXHR(async, method, path) { + var xhr = new XMLHttpRequest(); + xhr.open(method, "http://localhost:" + pSame + path, async); + return xhr; +} + +function checkResults(xhr, method, status, unsafe) { + if (unsafe) { + if (sRedirectPromptPref) { + // The method is null if we prompt for unsafe redirects + method = null; + } else { + // The status code is 200 when we don't prompt for unsafe redirects + status = 200; + } + } + + if (xhr.readyState != 4) { + return false; + } + Assert.equal(xhr.status, status); + + if (status == 200) { + // if followed then check for echoed method name + Assert.equal(xhr.getResponseHeader("X-Received-Method"), method); + } + + return true; +} + +function run_test() { + // start servers + sSame = new HttpServer(); + + // same-origin redirects + sSame.registerPathHandler( + "/bug" + BUGID + "-redirect301", + bug676059redirect301 + ); + sSame.registerPathHandler( + "/bug" + BUGID + "-redirect302", + bug676059redirect302 + ); + sSame.registerPathHandler( + "/bug" + BUGID + "-redirect303", + bug676059redirect303 + ); + sSame.registerPathHandler( + "/bug" + BUGID + "-redirect307", + bug676059redirect307 + ); + sSame.registerPathHandler( + "/bug" + BUGID + "-redirect308", + bug676059redirect308 + ); + + // cross-origin redirects + sSame.registerPathHandler( + "/bug" + OTHERBUGID + "-redirect301", + bug696849redirect301 + ); + sSame.registerPathHandler( + "/bug" + OTHERBUGID + "-redirect302", + bug696849redirect302 + ); + sSame.registerPathHandler( + "/bug" + OTHERBUGID + "-redirect303", + bug696849redirect303 + ); + sSame.registerPathHandler( + "/bug" + OTHERBUGID + "-redirect307", + bug696849redirect307 + ); + sSame.registerPathHandler( + "/bug" + OTHERBUGID + "-redirect308", + bug696849redirect308 + ); + + // same-origin target + sSame.registerPathHandler("/bug" + BUGID + "-target", echoMethod); + sSame.start(-1); + + // cross-origin target + sOther = new HttpServer(); + sOther.registerPathHandler("/bug" + OTHERBUGID + "-target", echoMethod); + sOther.start(-1); + + // format: redirectType, methodToSend, redirectedMethod, finalStatus + // redirectType sets the URI the initial request goes to + // methodToSend is the HTTP method to send + // redirectedMethod is the method to use for the redirect, if any + // finalStatus is 200 when the redirect takes place, redirectType otherwise + + // Note that unsafe methods should not follow the redirect automatically + // Of the methods below, DELETE, POST and PUT are unsafe + + sRedirectPromptPref = Preferences.get("network.http.prompt-temp-redirect"); + // Following Bug 677754 we don't prompt for unsafe redirects + + // same-origin variant + var tests = [ + // 301: rewrite just POST + [301, "DELETE", "DELETE", 301, true], + [301, "GET", "GET", 200, false], + [301, "HEAD", "HEAD", 200, false], + [301, "POST", "GET", 200, false], + [301, "PUT", "PUT", 301, true], + [301, "PROPFIND", "PROPFIND", 200, false], + // 302: see 301 + [302, "DELETE", "DELETE", 302, true], + [302, "GET", "GET", 200, false], + [302, "HEAD", "HEAD", 200, false], + [302, "POST", "GET", 200, false], + [302, "PUT", "PUT", 302, true], + [302, "PROPFIND", "PROPFIND", 200, false], + // 303: rewrite to GET except HEAD + [303, "DELETE", "GET", 200, false], + [303, "GET", "GET", 200, false], + [303, "HEAD", "HEAD", 200, false], + [303, "POST", "GET", 200, false], + [303, "PUT", "GET", 200, false], + [303, "PROPFIND", "GET", 200, false], + // 307: never rewrite + [307, "DELETE", "DELETE", 307, true], + [307, "GET", "GET", 200, false], + [307, "HEAD", "HEAD", 200, false], + [307, "POST", "POST", 307, true], + [307, "PUT", "PUT", 307, true], + [307, "PROPFIND", "PROPFIND", 200, false], + // 308: never rewrite + [308, "DELETE", "DELETE", 308, true], + [308, "GET", "GET", 200, false], + [308, "HEAD", "HEAD", 200, false], + [308, "POST", "POST", 308, true], + [308, "PUT", "PUT", 308, true], + [308, "PROPFIND", "PROPFIND", 200, false], + ]; + + // cross-origin variant + var othertests = tests; // for now these have identical results + + var xhr; + + for (let i = 0; i < tests.length; ++i) { + dump("Testing " + tests[i] + "\n"); + xhr = createXHR( + false, + tests[i][1], + "/bug" + BUGID + "-redirect" + tests[i][0] + ); + xhr.send(null); + checkResults(xhr, tests[i][2], tests[i][3], tests[i][4]); + } + + for (let i = 0; i < othertests.length; ++i) { + dump("Testing " + othertests[i] + " (cross-origin)\n"); + xhr = createXHR( + false, + othertests[i][1], + "/bug" + OTHERBUGID + "-redirect" + othertests[i][0] + ); + xhr.send(null); + checkResults(xhr, othertests[i][2], tests[i][3], tests[i][4]); + } + + sSame.stop(do_test_finished); + sOther.stop(do_test_finished); +} + +function redirect(metadata, response, status, port, bugid) { + // set a proper reason string to avoid confusion when looking at the + // HTTP messages + var reason; + if (status == 301) { + reason = "Moved Permanently"; + } else if (status == 302) { + reason = "Found"; + } else if (status == 303) { + reason = "See Other"; + } else if (status == 307) { + reason = "Temporary Redirect"; + } else if (status == 308) { + reason = "Permanent Redirect"; + } + + response.setStatusLine(metadata.httpVersion, status, reason); + response.setHeader( + "Location", + "http://localhost:" + port + "/bug" + bugid + "-target" + ); +} + +// PATH HANDLER FOR /bug676059-redirect301 +function bug676059redirect301(metadata, response) { + redirect(metadata, response, 301, pSame, BUGID); +} + +// PATH HANDLER FOR /bug696849-redirect301 +function bug696849redirect301(metadata, response) { + redirect(metadata, response, 301, pOther, OTHERBUGID); +} + +// PATH HANDLER FOR /bug676059-redirect302 +function bug676059redirect302(metadata, response) { + redirect(metadata, response, 302, pSame, BUGID); +} + +// PATH HANDLER FOR /bug696849-redirect302 +function bug696849redirect302(metadata, response) { + redirect(metadata, response, 302, pOther, OTHERBUGID); +} + +// PATH HANDLER FOR /bug676059-redirect303 +function bug676059redirect303(metadata, response) { + redirect(metadata, response, 303, pSame, BUGID); +} + +// PATH HANDLER FOR /bug696849-redirect303 +function bug696849redirect303(metadata, response) { + redirect(metadata, response, 303, pOther, OTHERBUGID); +} + +// PATH HANDLER FOR /bug676059-redirect307 +function bug676059redirect307(metadata, response) { + redirect(metadata, response, 307, pSame, BUGID); +} + +// PATH HANDLER FOR /bug676059-redirect308 +function bug676059redirect308(metadata, response) { + redirect(metadata, response, 308, pSame, BUGID); +} + +// PATH HANDLER FOR /bug696849-redirect307 +function bug696849redirect307(metadata, response) { + redirect(metadata, response, 307, pOther, OTHERBUGID); +} + +// PATH HANDLER FOR /bug696849-redirect308 +function bug696849redirect308(metadata, response) { + redirect(metadata, response, 308, pOther, OTHERBUGID); +} + +// Echo the request method in "X-Received-Method" header field +function echoMethod(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("X-Received-Method", metadata.method); +} diff --git a/netwerk/test/unit/test_about_networking.js b/netwerk/test/unit/test_about_networking.js new file mode 100644 index 0000000000..c70b7ead02 --- /dev/null +++ b/netwerk/test/unit/test_about_networking.js @@ -0,0 +1,118 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const gDashboard = Cc["@mozilla.org/network/dashboard;1"].getService( + Ci.nsIDashboard +); + +const gServerSocket = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket +); +const gHttpServer = new HttpServer(); + +add_test(function test_http() { + gDashboard.requestHttpConnections(function (data) { + let found = false; + for (let i = 0; i < data.connections.length; i++) { + if (data.connections[i].host == "localhost") { + found = true; + break; + } + } + Assert.equal(found, true); + + run_next_test(); + }); +}); + +add_test(function test_dns() { + gDashboard.requestDNSInfo(function (data) { + let found = false; + for (let i = 0; i < data.entries.length; i++) { + if (data.entries[i].hostname == "localhost") { + found = true; + break; + } + } + Assert.equal(found, true); + + do_test_pending(); + gHttpServer.stop(do_test_finished); + + run_next_test(); + }); +}); + +add_test(function test_sockets() { + // TODO: enable this test in bug 1581892. + if (mozinfo.socketprocess_networking) { + info("skip test_sockets"); + run_next_test(); + return; + } + + let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( + Ci.nsISocketTransportService + ); + let threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + + let transport = sts.createTransport( + [], + "127.0.0.1", + gServerSocket.port, + null, + null + ); + let listener = { + onTransportStatus(aTransport, aStatus, aProgress, aProgressMax) { + if (aStatus == Ci.nsISocketTransport.STATUS_CONNECTED_TO) { + gDashboard.requestSockets(function (data) { + gServerSocket.close(); + let found = false; + for (let i = 0; i < data.sockets.length; i++) { + if (data.sockets[i].host == "127.0.0.1") { + found = true; + break; + } + } + Assert.equal(found, true); + + run_next_test(); + }); + } + }, + }; + transport.setEventSink(listener, threadManager.currentThread); + + transport.openOutputStream(Ci.nsITransport.OPEN_BLOCKING, 0, 0); +}); + +function run_test() { + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // We always resolve localhost as it's hardcoded without the following pref: + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + + gHttpServer.start(-1); + + let uri = Services.io.newURI( + "http://localhost:" + gHttpServer.identity.primaryPort + ); + let channel = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + + channel.open(); + + gServerSocket.init(-1, true, -1); + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + + run_next_test(); +} diff --git a/netwerk/test/unit/test_about_protocol.js b/netwerk/test/unit/test_about_protocol.js new file mode 100644 index 0000000000..7fc6d63c1e --- /dev/null +++ b/netwerk/test/unit/test_about_protocol.js @@ -0,0 +1,49 @@ +/* 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"; + +var unsafeAboutModule = { + QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]), + newChannel(aURI, aLoadInfo) { + var uri = Services.io.newURI("about:blank"); + let chan = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo); + chan.owner = Services.scriptSecurityManager.getSystemPrincipal(); + return chan; + }, + getURIFlags(aURI) { + return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT; + }, +}; + +var factory = { + createInstance(aIID) { + return unsafeAboutModule.QueryInterface(aIID); + }, + QueryInterface: ChromeUtils.generateQI(["nsIFactory"]), +}; + +function run_test() { + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + let classID = Services.uuid.generateUUID(); + registrar.registerFactory( + classID, + "", + "@mozilla.org/network/protocol/about;1?what=unsafe", + factory + ); + + let aboutUnsafeChan = NetUtil.newChannel({ + uri: "about:unsafe", + loadUsingSystemPrincipal: true, + }); + + Assert.equal( + null, + aboutUnsafeChan.owner, + "URI_SAFE_FOR_UNTRUSTED_CONTENT channel has no owner" + ); + + registrar.unregisterFactory(classID, factory); +} diff --git a/netwerk/test/unit/test_aboutblank.js b/netwerk/test/unit/test_aboutblank.js new file mode 100644 index 0000000000..2d5c92f095 --- /dev/null +++ b/netwerk/test/unit/test_aboutblank.js @@ -0,0 +1,32 @@ +"use strict"; + +function run_test() { + var base = NetUtil.newURI("http://www.example.com"); + var about1 = NetUtil.newURI("about:blank"); + var about2 = NetUtil.newURI("about:blank", null, base); + + var chan1 = NetUtil.newChannel({ + uri: about1, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIPropertyBag2); + + var chan2 = NetUtil.newChannel({ + uri: about2, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIPropertyBag2); + + var haveProp = false; + var propVal = null; + try { + propVal = chan1.getPropertyAsInterface("baseURI", Ci.nsIURI); + haveProp = true; + } catch (e) { + if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw e; + } + // Property shouldn't be there. + } + Assert.equal(propVal, null); + Assert.equal(haveProp, false); + Assert.equal(chan2.getPropertyAsInterface("baseURI", Ci.nsIURI), base); +} diff --git a/netwerk/test/unit/test_addr_in_use_error.js b/netwerk/test/unit/test_addr_in_use_error.js new file mode 100644 index 0000000000..d25860e896 --- /dev/null +++ b/netwerk/test/unit/test_addr_in_use_error.js @@ -0,0 +1,36 @@ +// Opening a second listening socket on the same address as an extant +// socket should elicit NS_ERROR_SOCKET_ADDRESS_IN_USE on non-Windows +// machines. + +"use strict"; + +var CC = Components.Constructor; + +const ServerSocket = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init" +); + +function testAddrInUse() { + // Windows lets us have as many sockets listening on the same address as + // we like, evidently. + if (mozinfo.os == "win") { + return; + } + + // Create listening socket: + // any port (-1), loopback only (true), default backlog (-1) + let listener = ServerSocket(-1, true, -1); + Assert.ok(listener instanceof Ci.nsIServerSocket); + + // Try to create another listening socket on the same port, whatever that was. + do_check_throws_nsIException( + () => ServerSocket(listener.port, true, -1), + "NS_ERROR_SOCKET_ADDRESS_IN_USE" + ); +} + +function run_test() { + testAddrInUse(); +} diff --git a/netwerk/test/unit/test_alt-data_closeWithStatus.js b/netwerk/test/unit/test_alt-data_closeWithStatus.js new file mode 100644 index 0000000000..1bf799698e --- /dev/null +++ b/netwerk/test/unit/test_alt-data_closeWithStatus.js @@ -0,0 +1,182 @@ +/** + * Test for the "alternative data stream" - closing the stream with an error. + * + * - we load a URL with preference for an alt data (check what we get is the raw data, + * since there was nothing previously cached) + * - we store something in alt data (using the asyncWait method) + * - then we abort the operation calling closeWithStatus() + * - we flush the HTTP cache + * - we reload the same URL using a new channel, again prefering the alt data be loaded + * - again we receive the data from the server. + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +var httpServer = null; + +// needs to be rooted +var cacheFlushObserver = (cacheFlushObserver = { + observe() { + cacheFlushObserver = null; + readServerContentAgain(); + }, +}); + +var currentThread = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +const responseContent = "response body"; +const responseContent2 = "response body 2"; +const altContent = "!@#$%^&*()"; +const altContentType = "text/binary"; + +var shouldPassRevalidation = true; + +var cache_storage = null; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("ETag", "test-etag1"); + + let etag; + try { + etag = metadata.getHeader("If-None-Match"); + } catch (ex) { + etag = ""; + } + + if (etag == "test-etag1" && shouldPassRevalidation) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + } else { + var content = shouldPassRevalidation ? responseContent : responseContent2; + response.bodyOutputStream.write(content, content.length); + } +} + +function check_has_alt_data_in_index(aHasAltData, callback) { + if (inChildProcess()) { + callback(); + return; + } + + syncWithCacheIOThread(() => { + var hasAltData = {}; + cache_storage.getCacheIndexEntryAttrs(createURI(URL), "", hasAltData, {}); + Assert.equal(hasAltData.value, aHasAltData); + callback(); + }, true); +} + +function run_test() { + do_get_profile(); + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + do_test_pending(); + + if (!inChildProcess()) { + cache_storage = getCacheStorage("disk"); + wait_for_cache_index(asyncOpen); + } else { + asyncOpen(); + } +} + +function asyncOpen() { + var chan = make_channel(URL); + + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + altContentType, + "", + Ci.nsICacheInfoChannel.ASYNC + ); + + chan.asyncOpen(new ChannelListener(readServerContent, null)); +} + +function readServerContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + check_has_alt_data_in_index(false, () => { + if (!inChildProcess()) { + currentThread = Services.tm.currentThread; + } + + executeSoon(() => { + var os = cc.openAlternativeOutputStream( + altContentType, + altContent.length + ); + + var aos = os.QueryInterface(Ci.nsIAsyncOutputStream); + aos.asyncWait( + _ => { + os.write(altContent, altContent.length); + aos.closeWithStatus(Cr.NS_ERROR_FAILURE); + executeSoon(flushAndReadServerContentAgain); + }, + 0, + 0, + currentThread + ); + }); + }); +} + +function flushAndReadServerContentAgain() { + // We need to do a GC pass to ensure the cache entry has been freed. + gc(); + if (!inChildProcess()) { + Services.cache2 + .QueryInterface(Ci.nsICacheTesting) + .flush(cacheFlushObserver); + } else { + do_send_remote_message("flush"); + do_await_remote_message("flushed").then(() => { + readServerContentAgain(); + }); + } +} + +function readServerContentAgain() { + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + "dummy1", + "text/javascript", + Ci.nsICacheInfoChannel.ASYNC + ); + cc.preferAlternativeDataType( + altContentType, + "text/plain", + Ci.nsICacheInfoChannel.ASYNC + ); + cc.preferAlternativeDataType("dummy2", "", Ci.nsICacheInfoChannel.ASYNC); + + chan.asyncOpen(new ChannelListener(readServerContentAgainCB, null)); +} + +function readServerContentAgainCB(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + check_has_alt_data_in_index(false, () => httpServer.stop(do_test_finished)); +} diff --git a/netwerk/test/unit/test_alt-data_cross_process.js b/netwerk/test/unit/test_alt-data_cross_process.js new file mode 100644 index 0000000000..f108a7fc71 --- /dev/null +++ b/netwerk/test/unit/test_alt-data_cross_process.js @@ -0,0 +1,151 @@ +/** + * Test for the "alternative data stream" stored withing a cache entry. + * + * - we load a URL with preference for an alt data (check what we get is the raw data, + * since there was nothing previously cached) + * - we store the alt data along the channel (to the cache entry) + * - we flush the HTTP cache + * - we reload the same URL using a new channel, again prefering the alt data be loaded + * - this time the alt data must arive + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +const responseContent = "response body"; +const responseContent2 = "response body 2"; +const altContent = "!@#$%^&*()"; +const altContentType = "text/binary"; + +var servedNotModified = false; +var shouldPassRevalidation = true; + +var cache_storage = null; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("ETag", "test-etag1"); + + let etag; + try { + etag = metadata.getHeader("If-None-Match"); + } catch (ex) { + etag = ""; + } + + if (etag == "test-etag1" && shouldPassRevalidation) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + servedNotModified = true; + } else { + var content = shouldPassRevalidation ? responseContent : responseContent2; + response.bodyOutputStream.write(content, content.length); + } +} + +function check_has_alt_data_in_index(aHasAltData, callback) { + if (inChildProcess()) { + callback(); + return; + } + + syncWithCacheIOThread(() => { + var hasAltData = {}; + cache_storage.getCacheIndexEntryAttrs(createURI(URL), "", hasAltData, {}); + Assert.equal(hasAltData.value, aHasAltData); + callback(); + }, true); +} + +// This file is loaded as part of test_alt-data_cross_process_wrap.js. +// eslint-disable-next-line no-unused-vars +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + do_test_pending(); + + asyncOpen(); +} + +function asyncOpen() { + var chan = make_channel(URL); + + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + altContentType, + "", + Ci.nsICacheInfoChannel.ASYNC + ); + + chan.asyncOpen(new ChannelListener(readServerContent, null)); +} + +function readServerContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + check_has_alt_data_in_index(false, () => { + executeSoon(() => { + var os = cc.openAlternativeOutputStream( + altContentType, + altContent.length + ); + os.write(altContent, altContent.length); + os.close(); + + executeSoon(flushAndOpenAltChannel); + }); + }); +} + +function flushAndOpenAltChannel() { + // We need to do a GC pass to ensure the cache entry has been freed. + gc(); + do_send_remote_message("flush"); + do_await_remote_message("flushed").then(() => { + openAltChannel(); + }); +} + +function openAltChannel() { + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + altContentType, + "", + Ci.nsICacheInfoChannel.ASYNC + ); + + chan.asyncOpen(new ChannelListener(readAltContent, null)); +} + +function readAltContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(servedNotModified, true); + Assert.equal(cc.alternativeDataType, altContentType); + Assert.equal(buffer, altContent); + + // FINISH + do_send_remote_message("done"); + do_await_remote_message("finish").then(() => { + httpServer.stop(do_test_finished); + }); +} diff --git a/netwerk/test/unit/test_alt-data_overwrite.js b/netwerk/test/unit/test_alt-data_overwrite.js new file mode 100644 index 0000000000..bf8a2775a7 --- /dev/null +++ b/netwerk/test/unit/test_alt-data_overwrite.js @@ -0,0 +1,207 @@ +/** + * Test for overwriting the alternative data in a cache entry. + * + * - run_test loads a new channel + * - readServerContent checks the content, and saves alt-data + * - cacheFlushObserver creates a new channel with "text/binary" alt-data type + * - readAltContent checks that it gets back alt-data and creates a channel with the dummy/null alt-data type + * - readServerContent2 checks that it gets regular content, from the cache and tries to overwrite the alt-data with the same representation + * - cacheFlushObserver2 creates a new channel with "text/binary" alt-data type + * - readAltContent2 checks that it gets back alt-data, and tries to overwrite with a kind of alt-data + * - cacheFlushObserver3 creates a new channel with "text/binary2" alt-data type + * - readAltContent3 checks that it gets back the newly saved alt-data + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +let httpServer = null; + +function make_and_open_channel(url, altContentType, callback) { + let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); + if (altContentType) { + let cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + altContentType, + "", + Ci.nsICacheInfoChannel.ASYNC + ); + } + chan.asyncOpen(new ChannelListener(callback, null)); +} + +const responseContent = "response body"; +const altContent = "!@#$%^&*()"; +const altContentType = "text/binary"; +const altContent2 = "abc"; +const altContentType2 = "text/binary2"; + +let servedNotModified = false; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("ETag", "test-etag1"); + let etag = ""; + try { + etag = metadata.getHeader("If-None-Match"); + } catch (ex) { + etag = ""; + } + + if (etag == "test-etag1") { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + servedNotModified = true; + } else { + servedNotModified = false; + response.bodyOutputStream.write(responseContent, responseContent.length); + } +} + +function run_test() { + do_get_profile(); + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + do_test_pending(); + make_and_open_channel(URL, altContentType, readServerContent); +} + +function readServerContent(request, buffer) { + let cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + + executeSoon(() => { + let os = cc.openAlternativeOutputStream(altContentType, altContent.length); + os.write(altContent, altContent.length); + os.close(); + + executeSoon(flushAndOpenAltChannel); + }); +} + +function flushAndOpenAltChannel() { + // We need to do a GC pass to ensure the cache entry has been freed. + Cu.forceShrinkingGC(); + Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(cacheFlushObserver); +} + +// needs to be rooted +let cacheFlushObserver = { + observe() { + if (!cacheFlushObserver) { + info("ignoring cacheFlushObserver\n"); + return; + } + cacheFlushObserver = null; + Cu.forceShrinkingGC(); + make_and_open_channel(URL, altContentType, readAltContent); + }, +}; + +function readAltContent(request, buffer, closure, fromCache) { + Cu.forceShrinkingGC(); + let cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(fromCache || servedNotModified, true); + Assert.equal(cc.alternativeDataType, altContentType); + Assert.equal(buffer, altContent); + + make_and_open_channel(URL, "dummy/null", readServerContent2); +} + +function readServerContent2(request, buffer, closure, fromCache) { + Cu.forceShrinkingGC(); + let cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(fromCache || servedNotModified, true); + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + + executeSoon(() => { + let os = cc.openAlternativeOutputStream(altContentType, altContent.length); + os.write(altContent, altContent.length); + os.close(); + + executeSoon(flushAndOpenAltChannel2); + }); +} + +function flushAndOpenAltChannel2() { + // We need to do a GC pass to ensure the cache entry has been freed. + Cu.forceShrinkingGC(); + Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(cacheFlushObserver2); +} + +// needs to be rooted +let cacheFlushObserver2 = { + observe() { + if (!cacheFlushObserver2) { + info("ignoring cacheFlushObserver2\n"); + return; + } + cacheFlushObserver2 = null; + Cu.forceShrinkingGC(); + make_and_open_channel(URL, altContentType, readAltContent2); + }, +}; + +function readAltContent2(request, buffer, closure, fromCache) { + Cu.forceShrinkingGC(); + let cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(servedNotModified || fromCache, true); + Assert.equal(cc.alternativeDataType, altContentType); + Assert.equal(buffer, altContent); + + executeSoon(() => { + Cu.forceShrinkingGC(); + info("writing other content\n"); + let os = cc.openAlternativeOutputStream( + altContentType2, + altContent2.length + ); + os.write(altContent2, altContent2.length); + os.close(); + + executeSoon(flushAndOpenAltChannel3); + }); +} + +function flushAndOpenAltChannel3() { + // We need to do a GC pass to ensure the cache entry has been freed. + Cu.forceShrinkingGC(); + Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(cacheFlushObserver3); +} + +// needs to be rooted +let cacheFlushObserver3 = { + observe() { + if (!cacheFlushObserver3) { + info("ignoring cacheFlushObserver3\n"); + return; + } + + cacheFlushObserver3 = null; + Cu.forceShrinkingGC(); + make_and_open_channel(URL, altContentType2, readAltContent3); + }, +}; + +function readAltContent3(request, buffer, closure, fromCache) { + let cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(servedNotModified || fromCache, true); + Assert.equal(cc.alternativeDataType, altContentType2); + Assert.equal(buffer, altContent2); + + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_alt-data_simple.js b/netwerk/test/unit/test_alt-data_simple.js new file mode 100644 index 0000000000..97fe469e4a --- /dev/null +++ b/netwerk/test/unit/test_alt-data_simple.js @@ -0,0 +1,210 @@ +/** + * Test for the "alternative data stream" stored withing a cache entry. + * + * - we load a URL with preference for an alt data (check what we get is the raw data, + * since there was nothing previously cached) + * - we store the alt data along the channel (to the cache entry) + * - we flush the HTTP cache + * - we reload the same URL using a new channel, again prefering the alt data be loaded + * - this time the alt data must arive + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +const responseContent = "response body"; +const responseContent2 = "response body 2"; +const altContent = "!@#$%^&*()"; +const altContentType = "text/binary"; + +var servedNotModified = false; +var shouldPassRevalidation = true; + +var cache_storage = null; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("ETag", "test-etag1"); + + let etag; + try { + etag = metadata.getHeader("If-None-Match"); + } catch (ex) { + etag = ""; + } + + if (etag == "test-etag1" && shouldPassRevalidation) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + servedNotModified = true; + } else { + var content = shouldPassRevalidation ? responseContent : responseContent2; + response.bodyOutputStream.write(content, content.length); + } +} + +function check_has_alt_data_in_index(aHasAltData, callback) { + if (inChildProcess()) { + callback(); + return; + } + + syncWithCacheIOThread(() => { + var hasAltData = {}; + cache_storage.getCacheIndexEntryAttrs(createURI(URL), "", hasAltData, {}); + Assert.equal(hasAltData.value, aHasAltData); + callback(); + }, true); +} + +function run_test() { + do_get_profile(); + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + do_test_pending(); + + if (!inChildProcess()) { + cache_storage = getCacheStorage("disk"); + wait_for_cache_index(asyncOpen); + } else { + asyncOpen(); + } +} + +function asyncOpen() { + var chan = make_channel(URL); + + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + altContentType, + "", + Ci.nsICacheInfoChannel.ASYNC + ); + + chan.asyncOpen(new ChannelListener(readServerContent, null)); +} + +function readServerContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + check_has_alt_data_in_index(false, () => { + executeSoon(() => { + var os = cc.openAlternativeOutputStream( + altContentType, + altContent.length + ); + os.write(altContent, altContent.length); + os.close(); + + executeSoon(flushAndOpenAltChannel); + }); + }); +} + +// needs to be rooted +var cacheFlushObserver = (cacheFlushObserver = { + observe() { + cacheFlushObserver = null; + openAltChannel(); + }, +}); + +function flushAndOpenAltChannel() { + // We need to do a GC pass to ensure the cache entry has been freed. + gc(); + if (!inChildProcess()) { + Services.cache2 + .QueryInterface(Ci.nsICacheTesting) + .flush(cacheFlushObserver); + } else { + do_send_remote_message("flush"); + do_await_remote_message("flushed").then(() => { + openAltChannel(); + }); + } +} + +function openAltChannel() { + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + "dummy1", + "text/javascript", + Ci.nsICacheInfoChannel.ASYNC + ); + cc.preferAlternativeDataType( + altContentType, + "text/plain", + Ci.nsICacheInfoChannel.ASYNC + ); + cc.preferAlternativeDataType("dummy2", "", Ci.nsICacheInfoChannel.ASYNC); + + chan.asyncOpen(new ChannelListener(readAltContent, null)); +} + +function readAltContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(servedNotModified, true); + Assert.equal(cc.alternativeDataType, altContentType); + Assert.equal(buffer, altContent); + check_has_alt_data_in_index(true, () => { + cc.getOriginalInputStream({ + onInputStreamReady(aInputStream) { + executeSoon(() => readOriginalInputStream(aInputStream)); + }, + }); + }); +} + +function readOriginalInputStream(aInputStream) { + // We expect the async stream length to match the expected content. + // If the test times out, it's probably because of this. + try { + let originalData = read_stream(aInputStream, responseContent.length); + Assert.equal(originalData, responseContent); + requestAgain(); + } catch (e) { + equal(e.result, Cr.NS_BASE_STREAM_WOULD_BLOCK); + executeSoon(() => readOriginalInputStream(aInputStream)); + } +} + +function requestAgain() { + shouldPassRevalidation = false; + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + altContentType, + "", + Ci.nsICacheInfoChannel.ASYNC + ); + chan.asyncOpen(new ChannelListener(readEmptyAltContent, null)); +} + +function readEmptyAltContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + // the cache is overwrite and the alt-data is reset + Assert.equal(cc.alternativeDataType, ""); + Assert.equal(buffer, responseContent2); + check_has_alt_data_in_index(false, () => httpServer.stop(do_test_finished)); +} diff --git a/netwerk/test/unit/test_alt-data_stream.js b/netwerk/test/unit/test_alt-data_stream.js new file mode 100644 index 0000000000..7f5f5394f4 --- /dev/null +++ b/netwerk/test/unit/test_alt-data_stream.js @@ -0,0 +1,161 @@ +/** + * Test for the "alternative data stream" stored withing a cache entry. + * + * - we load a URL with preference for an alt data (check what we get is the raw data, + * since there was nothing previously cached) + * - we write a big chunk of alt-data to the output stream + * - we load the URL again, expecting to get alt-data + * - we check that the alt-data is streamed. We should get the first chunk, then + * the rest of the alt-data is written, and we check that it is received in + * the proper order. + * + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +var httpServer = null; + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseContent = "response body"; +// We need a large content in order to make sure that the IPDL stream is cut +// into several different chunks. +// We fill each chunk with a different character for easy debugging. +const altContent = + "a".repeat(128 * 1024) + + "b".repeat(128 * 1024) + + "c".repeat(128 * 1024) + + "d".repeat(128 * 1024) + + "e".repeat(128 * 1024) + + "f".repeat(128 * 1024) + + "g".repeat(128 * 1024) + + "h".repeat(128 * 1024) + + "i".repeat(13); // Just so the chunk size doesn't match exactly. + +const firstChunkSize = Math.floor(altContent.length / 4); +const altContentType = "text/binary"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "max-age=86400"); + + response.bodyOutputStream.write(responseContent, responseContent.length); +} + +function run_test() { + do_get_profile(); + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan = make_channel(URL); + + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + altContentType, + "", + Ci.nsICacheInfoChannel.ASYNC + ); + + chan.asyncOpen(new ChannelListener(readServerContent, null)); + do_test_pending(); +} + +// Output stream used to write alt-data to the cache entry. +var os; + +function readServerContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + + executeSoon(() => { + os = cc.openAlternativeOutputStream(altContentType, altContent.length); + // Write a quarter of the alt data content + os.write(altContent, firstChunkSize); + + executeSoon(openAltChannel); + }); +} + +function openAltChannel() { + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + altContentType, + "", + Ci.nsICacheInfoChannel.ASYNC + ); + + chan.asyncOpen(altDataListener); +} + +var altDataListener = { + buffer: "", + onStartRequest(request) {}, + onDataAvailable(request, stream, offset, count) { + let string = NetUtil.readInputStreamToString(stream, count); + this.buffer += string; + + // XXX: this condition might be a bit volatile. If this test times out, + // it probably means that for some reason, the listener didn't get all the + // data in the first chunk. + if (this.buffer.length == firstChunkSize) { + // write the rest of the content + os.write( + altContent.substring(firstChunkSize, altContent.length), + altContent.length - firstChunkSize + ); + os.close(); + } + }, + onStopRequest(request, status) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + Assert.equal(cc.alternativeDataType, altContentType); + Assert.equal(this.buffer.length, altContent.length); + Assert.equal(this.buffer, altContent); + openAltChannelWithOriginalContent(); + }, +}; + +function openAltChannelWithOriginalContent() { + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + altContentType, + "", + Ci.nsICacheInfoChannel.SERIALIZE + ); + + chan.asyncOpen(originalListener); +} + +var originalListener = { + buffer: "", + onStartRequest(request) {}, + onDataAvailable(request, stream, offset, count) { + let string = NetUtil.readInputStreamToString(stream, count); + this.buffer += string; + }, + onStopRequest(request, status) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + Assert.equal(cc.alternativeDataType, altContentType); + Assert.equal(this.buffer.length, responseContent.length); + Assert.equal(this.buffer, responseContent); + testAltDataStream(cc); + }, +}; + +function testAltDataStream(cc) { + Assert.ok(!!cc.alternativeDataInputStream); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_alt-data_too_big.js b/netwerk/test/unit/test_alt-data_too_big.js new file mode 100644 index 0000000000..fc9ca337fa --- /dev/null +++ b/netwerk/test/unit/test_alt-data_too_big.js @@ -0,0 +1,113 @@ +/** + * Test for handling too big alternative data + * + * - first we try to open an output stream for too big alt-data which must fail + * and leave original data intact + * + * - then we open the output stream without passing predicted data size which + * succeeds but writing must fail later at the size limit and the original + * data must be kept + */ + +"use strict"; + +var data = "data "; +var altData = "alt-data"; + +function run_test() { + do_get_profile(); + + // Expand both data to 1MB + for (let i = 0; i < 17; i++) { + data += data; + altData += altData; + } + + // Set the limit so that the data fits but alt-data doesn't. + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1800); + + write_data(); + + do_test_pending(); +} + +function write_data() { + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function (status, entry) { + Assert.equal(status, Cr.NS_OK); + + var os = entry.openOutputStream(0, -1); + var written = os.write(data, data.length); + Assert.equal(written, data.length); + os.close(); + + open_big_altdata_output(entry); + } + ); +} + +function open_big_altdata_output(entry) { + try { + entry.openAlternativeOutputStream("text/binary", altData.length); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_FILE_TOO_BIG); + } + entry.close(); + + check_entry(write_big_altdata); +} + +function write_big_altdata() { + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function (status, entry) { + Assert.equal(status, Cr.NS_OK); + + var os = entry.openAlternativeOutputStream("text/binary", -1); + try { + os.write(altData, altData.length); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_FILE_TOO_BIG); + } + os.close(); + entry.close(); + + check_entry(do_test_finished); + } + ); +} + +function check_entry(cb) { + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function (status, entry) { + Assert.equal(status, Cr.NS_OK); + + var is = null; + try { + is = entry.openAlternativeInputStream("text/binary"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_NOT_AVAILABLE); + } + + is = entry.openInputStream(0); + pumpReadStream(is, function (read) { + Assert.equal(read.length, data.length); + is.close(); + entry.close(); + + executeSoon(cb); + }); + } + ); +} diff --git a/netwerk/test/unit/test_altsvc.js b/netwerk/test/unit/test_altsvc.js new file mode 100644 index 0000000000..b52f9cdffc --- /dev/null +++ b/netwerk/test/unit/test_altsvc.js @@ -0,0 +1,595 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var h2Port; +var prefs; +var http2pref; +var altsvcpref1; +var altsvcpref2; + +// https://foo.example.com:(h2Port) +// https://bar.example.com:(h2Port) <- invalid for bar, but ok for foo +var h1Foo; // server http://foo.example.com:(h1Foo.identity.primaryPort) +var h1Bar; // server http://bar.example.com:(h1bar.identity.primaryPort) + +var otherServer; // server socket listening for other connection. + +var h2FooRoute; // foo.example.com:H2PORT +var h2BarRoute; // bar.example.com:H2PORT +var h2Route; // :H2PORT +var httpFooOrigin; // http://foo.exmaple.com:PORT/ +var httpsFooOrigin; // https://foo.exmaple.com:PORT/ +var httpBarOrigin; // http://bar.example.com:PORT/ +var httpsBarOrigin; // https://bar.example.com:PORT/ + +function run_test() { + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Services.prefs; + + http2pref = prefs.getBoolPref("network.http.http2.enabled"); + altsvcpref1 = prefs.getBoolPref("network.http.altsvc.enabled"); + altsvcpref2 = prefs.getBoolPref("network.http.altsvc.oe", true); + + prefs.setBoolPref("network.http.http2.enabled", true); + prefs.setBoolPref("network.http.altsvc.enabled", true); + prefs.setBoolPref("network.http.altsvc.oe", true); + prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, bar.example.com" + ); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. The same cert is used + // for both h2FooRoute and h2BarRoute though it is only valid for + // the foo.example.com domain name. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + h1Foo = new HttpServer(); + h1Foo.registerPathHandler("/altsvc-test", h1Server); + h1Foo.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK); + h1Foo.start(-1); + h1Foo.identity.setPrimary( + "http", + "foo.example.com", + h1Foo.identity.primaryPort + ); + + h1Bar = new HttpServer(); + h1Bar.registerPathHandler("/altsvc-test", h1Server); + h1Bar.start(-1); + h1Bar.identity.setPrimary( + "http", + "bar.example.com", + h1Bar.identity.primaryPort + ); + + h2FooRoute = "foo.example.com:" + h2Port; + h2BarRoute = "bar.example.com:" + h2Port; + h2Route = ":" + h2Port; + + httpFooOrigin = "http://foo.example.com:" + h1Foo.identity.primaryPort + "/"; + httpsFooOrigin = "https://" + h2FooRoute + "/"; + httpBarOrigin = "http://bar.example.com:" + h1Bar.identity.primaryPort + "/"; + httpsBarOrigin = "https://" + h2BarRoute + "/"; + dump( + "http foo - " + + httpFooOrigin + + "\n" + + "https foo - " + + httpsFooOrigin + + "\n" + + "http bar - " + + httpBarOrigin + + "\n" + + "https bar - " + + httpsBarOrigin + + "\n" + ); + + doTest1(); +} + +function h1Server(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + try { + var hval = "h2=" + metadata.getHeader("x-altsvc"); + response.setHeader("Alt-Svc", hval, false); + } catch (e) {} + + var body = "Q: What did 0 say to 8? A: Nice Belt!\n"; + response.bodyOutputStream.write(body, body.length); +} + +function h1ServerWK(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + var body = '["http://foo.example.com:' + h1Foo.identity.primaryPort + '"]'; + response.bodyOutputStream.write(body, body.length); +} + +function resetPrefs() { + prefs.setBoolPref("network.http.http2.enabled", http2pref); + prefs.setBoolPref("network.http.altsvc.enabled", altsvcpref1); + prefs.setBoolPref("network.http.altsvc.oe", altsvcpref2); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.security.ports.banned"); +} + +function makeChan(origin) { + return NetUtil.newChannel({ + uri: origin + "altsvc-test", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var origin; +var xaltsvc; +var loadWithoutClearingMappings = false; +var disallowH3 = false; +var disallowH2 = false; +var nextTest; +var expectPass = true; +var waitFor = 0; +var originAttributes = {}; + +var Listener = function () {}; +Listener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + if (expectPass) { + if (!Components.isSuccessCode(request.status)) { + do_throw( + "Channel should have a success code! (" + request.status + ")" + ); + } + Assert.equal(request.responseStatus, 200); + } else { + Assert.equal(Components.isSuccessCode(request.status), false); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + var routed = ""; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + Assert.equal(Components.isSuccessCode(status), expectPass); + + if (waitFor != 0) { + Assert.equal(routed, ""); + do_test_pending(); + loadWithoutClearingMappings = true; + do_timeout(waitFor, doTest); + waitFor = 0; + xaltsvc = "NA"; + } else if (xaltsvc == "NA") { + Assert.equal(routed, ""); + nextTest(); + } else if (routed == xaltsvc) { + Assert.equal(routed, xaltsvc); // always true, but a useful log + nextTest(); + } else { + dump("poll later for alt svc mapping\n"); + do_test_pending(); + loadWithoutClearingMappings = true; + do_timeout(500, doTest); + } + + do_test_finished(); + }, +}; + +function testsDone() { + dump("testDone\n"); + resetPrefs(); + do_test_pending(); + otherServer.close(); + do_test_pending(); + h1Foo.stop(do_test_finished); + do_test_pending(); + h1Bar.stop(do_test_finished); +} + +function doTest() { + dump("execute doTest " + origin + "\n"); + var chan = makeChan(origin); + var listener = new Listener(); + if (xaltsvc != "NA") { + chan.setRequestHeader("x-altsvc", xaltsvc, false); + } + if (loadWithoutClearingMappings) { + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + } else { + chan.loadFlags = + Ci.nsIRequest.LOAD_FRESH_CONNECTION | + Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + } + if (disallowH3) { + let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.allowHttp3 = false; + disallowH3 = false; + } + if (disallowH2) { + let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.allowSpdy = false; + disallowH2 = false; + } + loadWithoutClearingMappings = false; + chan.loadInfo.originAttributes = originAttributes; + chan.asyncOpen(listener); +} + +// xaltsvc is overloaded to do two things.. +// 1] it is sent in the x-altsvc request header, and the response uses the value in the Alt-Svc response header +// 2] the test polls until necko sets Alt-Used to that value (i.e. it uses that route) +// +// When xaltsvc is set to h2Route (i.e. :port with the implied hostname) it doesn't match the alt-used, +// which is always explicit, so it needs to be changed after the channel is created but before the +// listener is invoked + +// http://foo served from h2=:port +function doTest1() { + dump("doTest1()\n"); + origin = httpFooOrigin; + xaltsvc = h2Route; + nextTest = doTest2; + do_test_pending(); + doTest(); + xaltsvc = h2FooRoute; +} + +// http://foo served from h2=foo:port +function doTest2() { + dump("doTest2()\n"); + origin = httpFooOrigin; + xaltsvc = h2FooRoute; + nextTest = doTest3; + do_test_pending(); + doTest(); +} + +// http://foo served from h2=bar:port +// requires cert for foo +function doTest3() { + dump("doTest3()\n"); + origin = httpFooOrigin; + xaltsvc = h2BarRoute; + nextTest = doTest4; + do_test_pending(); + doTest(); +} + +// https://bar should fail because host bar has cert for foo +function doTest4() { + dump("doTest4()\n"); + origin = httpsBarOrigin; + xaltsvc = ""; + expectPass = false; + nextTest = doTest5; + do_test_pending(); + doTest(); +} + +// https://foo no alt-svc (just check cert setup) +function doTest5() { + dump("doTest5()\n"); + origin = httpsFooOrigin; + xaltsvc = "NA"; + expectPass = true; + nextTest = doTest6; + do_test_pending(); + doTest(); +} + +// https://foo via bar (bar has cert for foo) +function doTest6() { + dump("doTest6()\n"); + origin = httpsFooOrigin; + xaltsvc = h2BarRoute; + nextTest = doTest7; + do_test_pending(); + doTest(); +} + +// check again https://bar should fail because host bar has cert for foo +function doTest7() { + dump("doTest7()\n"); + origin = httpsBarOrigin; + xaltsvc = ""; + expectPass = false; + nextTest = doTest8; + do_test_pending(); + doTest(); +} + +// http://bar via h2 on bar +// should not use TLS/h2 because h2BarRoute is not auth'd for bar +// however the test ought to PASS (i.e. get a 200) because fallback +// to plaintext happens.. thus the timeout +function doTest8() { + dump("doTest8()\n"); + origin = httpBarOrigin; + xaltsvc = h2BarRoute; + expectPass = true; + waitFor = 500; + nextTest = doTest9; + do_test_pending(); + doTest(); +} + +// http://bar served from h2=:port, which is like the bar route in 8 +function doTest9() { + dump("doTest9()\n"); + origin = httpBarOrigin; + xaltsvc = h2Route; + expectPass = true; + waitFor = 500; + nextTest = doTest10; + do_test_pending(); + doTest(); + xaltsvc = h2BarRoute; +} + +// check again https://bar should fail because host bar has cert for foo +function doTest10() { + dump("doTest10()\n"); + origin = httpsBarOrigin; + xaltsvc = ""; + expectPass = false; + nextTest = doTest11; + do_test_pending(); + doTest(); +} + +// http://bar served from h2=foo, should fail because host foo only has +// cert for foo. Fail in this case means alt-svc is not used, but content +// is served +function doTest11() { + dump("doTest11()\n"); + origin = httpBarOrigin; + xaltsvc = h2FooRoute; + expectPass = true; + waitFor = 500; + nextTest = doTest12; + do_test_pending(); + doTest(); +} + +// Test 12-15: +// Insert a cache of http://foo served from h2=:port with origin attributes. +function doTest12() { + dump("doTest12()\n"); + origin = httpFooOrigin; + xaltsvc = h2Route; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + nextTest = doTest13; + do_test_pending(); + doTest(); + xaltsvc = h2FooRoute; +} + +// Make sure we get a cache miss with a different userContextId. +function doTest13() { + dump("doTest13()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + originAttributes = { + userContextId: 2, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest14; + do_test_pending(); + doTest(); +} + +// Make sure we get a cache miss with a different firstPartyDomain. +function doTest14() { + dump("doTest14()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + originAttributes = { + userContextId: 1, + firstPartyDomain: "b.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest15; + do_test_pending(); + doTest(); +} +// +// Make sure we get a cache hit with the same origin attributes. +function doTest15() { + dump("doTest15()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest16; + do_test_pending(); + doTest(); + // This ensures a cache hit. + xaltsvc = h2FooRoute; +} + +// Make sure we do not use H2 if it is disabled on a channel. +function doTest16() { + dump("doTest16()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + disallowH2 = true; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest17; + do_test_pending(); + doTest(); +} + +// Make sure we use H2 if only Http3 is disabled on a channel. +function doTest17() { + dump("doTest17()\n"); + origin = httpFooOrigin; + xaltsvc = h2Route; + disallowH3 = true; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest18; + do_test_pending(); + doTest(); + // This should ensures a cache hit. + xaltsvc = h2FooRoute; +} + +// Check we don't connect to blocked ports +function doTest18() { + dump("doTest18()\n"); + origin = httpFooOrigin; + nextTest = testsDone; + otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + otherServer.init(-1, true, -1); + xaltsvc = "localhost:" + otherServer.port; + Services.prefs.setCharPref( + "network.security.ports.banned", + "" + otherServer.port + ); + dump("Blocked port: " + otherServer.port); + waitFor = 500; + otherServer.asyncListen({ + onSocketAccepted() { + Assert.ok(false, "Got connection to socket when we didn't expect it!"); + }, + onStopListening() { + // We get closed when the entire file is done, which guarantees we get the socket accept + // if we do connect to the alt-svc header + do_test_finished(); + }, + }); + nextTest = doTest19; + do_test_pending(); + doTest(); +} + +// Check we don't connect to blocked ports +function doTest19() { + dump("doTest19()\n"); + origin = httpFooOrigin; + nextTest = testsDone; + otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + const BAD_PORT_U32 = 6667 + 65536; + otherServer.init(BAD_PORT_U32, true, -1); + Assert.ok(otherServer.port == 6667, "Trying to listen on port 6667"); + xaltsvc = "localhost:" + BAD_PORT_U32; + dump("Blocked port: " + otherServer.port); + waitFor = 500; + otherServer.asyncListen({ + onSocketAccepted() { + Assert.ok(false, "Got connection to socket when we didn't expect it!"); + }, + onStopListening() { + // We get closed when the entire file is done, which guarantees we get the socket accept + // if we do connect to the alt-svc header + do_test_finished(); + }, + }); + nextTest = doTest20; + do_test_pending(); + doTest(); +} +function doTest20() { + dump("doTest20()\n"); + origin = httpFooOrigin; + nextTest = testsDone; + otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + const BAD_PORT_U64 = 6666 + 429496729; + otherServer.init(6666, true, -1); + Assert.ok(otherServer.port == 6666, "Trying to listen on port 6666"); + xaltsvc = "localhost:" + BAD_PORT_U64; + dump("Blocked port: " + otherServer.port); + waitFor = 500; + otherServer.asyncListen({ + onSocketAccepted() { + Assert.ok(false, "Got connection to socket when we didn't expect it!"); + }, + onStopListening() { + // We get closed when the entire file is done, which guarantees we get the socket accept + // if we do connect to the alt-svc header + do_test_finished(); + }, + }); + nextTest = doTest21; + do_test_pending(); + doTest(); +} +// Port 65535 should be OK +function doTest21() { + dump("doTest21()\n"); + origin = httpFooOrigin; + nextTest = testsDone; + otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + const GOOD_PORT = 65535; + otherServer.init(65535, true, -1); + Assert.ok(otherServer.port == 65535, "Trying to listen on port 65535"); + xaltsvc = "localhost:" + GOOD_PORT; + dump("Allowed port: " + otherServer.port); + waitFor = 500; + otherServer.asyncListen({ + onSocketAccepted() { + Assert.ok(true, "Got connection to socket when we didn't expect it!"); + }, + onStopListening() { + // We get closed when the entire file is done, which guarantees we get the socket accept + // if we do connect to the alt-svc header + do_test_finished(); + }, + }); + do_test_pending(); + doTest(); +} diff --git a/netwerk/test/unit/test_altsvc_http3.js b/netwerk/test/unit/test_altsvc_http3.js new file mode 100644 index 0000000000..9d8d7ecac9 --- /dev/null +++ b/netwerk/test/unit/test_altsvc_http3.js @@ -0,0 +1,492 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var h3Port; + +// https://foo.example.com:(h3Port) +// https://bar.example.com:(h3Port) <- invalid for bar, but ok for foo +var h1Foo; // server http://foo.example.com:(h1Foo.identity.primaryPort) +var h1Bar; // server http://bar.example.com:(h1bar.identity.primaryPort) + +var otherServer; // server socket listening for other connection. + +var h3FooRoute; // foo.example.com:H3PORT +var h3BarRoute; // bar.example.com:H3PORT +var h3Route; // :H3PORT +var httpFooOrigin; // http://foo.exmaple.com:PORT/ +var httpsFooOrigin; // https://foo.exmaple.com:PORT/ +var httpBarOrigin; // http://bar.example.com:PORT/ +var httpsBarOrigin; // https://bar.example.com:PORT/ + +function run_test() { + h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + // Set to allow the cert presented by our H3 server + do_get_profile(); + + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setBoolPref("network.http.altsvc.enabled", true); + Services.prefs.setBoolPref("network.http.altsvc.oe", true); + Services.prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, bar.example.com" + ); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. The same cert is used + // for both h3FooRoute and h3BarRoute though it is only valid for + // the foo.example.com domain name. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + h1Foo = new HttpServer(); + h1Foo.registerPathHandler("/altsvc-test", h1Server); + h1Foo.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK); + h1Foo.start(-1); + h1Foo.identity.setPrimary( + "http", + "foo.example.com", + h1Foo.identity.primaryPort + ); + + h1Bar = new HttpServer(); + h1Bar.registerPathHandler("/altsvc-test", h1Server); + h1Bar.start(-1); + h1Bar.identity.setPrimary( + "http", + "bar.example.com", + h1Bar.identity.primaryPort + ); + + h3FooRoute = "foo.example.com:" + h3Port; + h3BarRoute = "bar.example.com:" + h3Port; + h3Route = ":" + h3Port; + + httpFooOrigin = "http://foo.example.com:" + h1Foo.identity.primaryPort + "/"; + httpsFooOrigin = "https://" + h3FooRoute + "/"; + httpBarOrigin = "http://bar.example.com:" + h1Bar.identity.primaryPort + "/"; + httpsBarOrigin = "https://" + h3BarRoute + "/"; + dump( + "http foo - " + + httpFooOrigin + + "\n" + + "https foo - " + + httpsFooOrigin + + "\n" + + "http bar - " + + httpBarOrigin + + "\n" + + "https bar - " + + httpsBarOrigin + + "\n" + ); + + doTest1(); +} + +function h1Server(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + try { + var hval = "h3-29=" + metadata.getHeader("x-altsvc"); + response.setHeader("Alt-Svc", hval, false); + } catch (e) {} + + var body = "Q: What did 0 say to 8? A: Nice Belt!\n"; + response.bodyOutputStream.write(body, body.length); +} + +function h1ServerWK(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + var body = '["http://foo.example.com:' + h1Foo.identity.primaryPort + '"]'; + response.bodyOutputStream.write(body, body.length); +} + +function resetPrefs() { + Services.prefs.clearUserPref("network.http.http3.enable"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.http.altsvc.enabled"); + Services.prefs.clearUserPref("network.http.altsvc.oe"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.security.ports.banned"); +} + +function makeChan(origin) { + return NetUtil.newChannel({ + uri: origin + "altsvc-test", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var origin; +var xaltsvc; +var loadWithoutClearingMappings = false; +var disallowH3 = false; +var disallowH2 = false; +var testKeepAliveNotSet = false; +var nextTest; +var expectPass = true; +var waitFor = 0; +var originAttributes = {}; + +var Listener = function () {}; +Listener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + if (expectPass) { + if (!Components.isSuccessCode(request.status)) { + do_throw( + "Channel should have a success code! (" + request.status + ")" + ); + } + Assert.equal(request.responseStatus, 200); + } else { + Assert.equal(Components.isSuccessCode(request.status), false); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + var routed = ""; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + Assert.equal(Components.isSuccessCode(status), expectPass); + + if (waitFor != 0) { + Assert.equal(routed, ""); + do_test_pending(); + loadWithoutClearingMappings = true; + do_timeout(waitFor, doTest); + waitFor = 0; + xaltsvc = "NA"; + } else if (xaltsvc == "NA") { + Assert.equal(routed, ""); + nextTest(); + } else if (routed == xaltsvc) { + Assert.equal(routed, xaltsvc); // always true, but a useful log + nextTest(); + } else { + dump("poll later for alt svc mapping\n"); + do_test_pending(); + loadWithoutClearingMappings = true; + do_timeout(500, doTest); + } + + do_test_finished(); + }, +}; + +function testsDone() { + dump("testDone\n"); + resetPrefs(); + do_test_pending(); + otherServer.close(); + do_test_pending(); + h1Foo.stop(do_test_finished); + do_test_pending(); + h1Bar.stop(do_test_finished); +} + +function doTest() { + dump("execute doTest " + origin + "\n"); + var chan = makeChan(origin); + var listener = new Listener(); + if (xaltsvc != "NA") { + chan.setRequestHeader("x-altsvc", xaltsvc, false); + } + if (testKeepAliveNotSet) { + chan.setRequestHeader("Connection", "close", false); + testKeepAliveNotSet = false; + } + if (loadWithoutClearingMappings) { + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + } else { + chan.loadFlags = + Ci.nsIRequest.LOAD_FRESH_CONNECTION | + Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + } + if (disallowH3) { + let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.allowHttp3 = false; + disallowH3 = false; + } + if (disallowH2) { + let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.allowSpdy = false; + disallowH2 = false; + } + loadWithoutClearingMappings = false; + chan.loadInfo.originAttributes = originAttributes; + chan.asyncOpen(listener); +} + +// xaltsvc is overloaded to do two things.. +// 1] it is sent in the x-altsvc request header, and the response uses the value in the Alt-Svc response header +// 2] the test polls until necko sets Alt-Used to that value (i.e. it uses that route) +// +// When xaltsvc is set to h3Route (i.e. :port with the implied hostname) it doesn't match the alt-used, +// which is always explicit, so it needs to be changed after the channel is created but before the +// listener is invoked + +// http://foo served from h3-29=:port +function doTest1() { + dump("doTest1()\n"); + origin = httpFooOrigin; + xaltsvc = h3Route; + nextTest = doTest2; + do_test_pending(); + doTest(); + xaltsvc = h3FooRoute; +} + +// http://foo served from h3-29=foo:port +function doTest2() { + dump("doTest2()\n"); + origin = httpFooOrigin; + xaltsvc = h3FooRoute; + nextTest = doTest3; + do_test_pending(); + doTest(); +} + +// http://foo served from h3-29=bar:port +// requires cert for foo +function doTest3() { + dump("doTest3()\n"); + origin = httpFooOrigin; + xaltsvc = h3BarRoute; + nextTest = doTest4; + do_test_pending(); + doTest(); +} + +// https://bar should fail because host bar has cert for foo +function doTest4() { + dump("doTest4()\n"); + origin = httpsBarOrigin; + xaltsvc = ""; + expectPass = false; + nextTest = doTest5; + do_test_pending(); + doTest(); +} + +// http://bar via h3 on bar +// should not use TLS/h3 because h3BarRoute is not auth'd for bar +// however the test ought to PASS (i.e. get a 200) because fallback +// to plaintext happens.. thus the timeout +function doTest5() { + dump("doTest5()\n"); + origin = httpBarOrigin; + xaltsvc = h3BarRoute; + expectPass = true; + waitFor = 500; + nextTest = doTest6; + do_test_pending(); + doTest(); +} + +// http://bar served from h3-29=:port, which is like the bar route in 8 +function doTest6() { + dump("doTest6()\n"); + origin = httpBarOrigin; + xaltsvc = h3Route; + expectPass = true; + waitFor = 500; + nextTest = doTest7; + do_test_pending(); + doTest(); + xaltsvc = h3BarRoute; +} + +// check again https://bar should fail because host bar has cert for foo +function doTest7() { + dump("doTest7()\n"); + origin = httpsBarOrigin; + xaltsvc = ""; + expectPass = false; + nextTest = doTest8; + do_test_pending(); + doTest(); +} + +// http://bar served from h3-29=foo, should fail because host foo only has +// cert for foo. Fail in this case means alt-svc is not used, but content +// is served +function doTest8() { + dump("doTest8()\n"); + origin = httpBarOrigin; + xaltsvc = h3FooRoute; + expectPass = true; + waitFor = 500; + nextTest = doTest9; + do_test_pending(); + doTest(); +} + +// Test 9-12: +// Insert a cache of http://foo served from h3-29=:port with origin attributes. +function doTest9() { + dump("doTest9()\n"); + origin = httpFooOrigin; + xaltsvc = h3Route; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + nextTest = doTest10; + do_test_pending(); + doTest(); + xaltsvc = h3FooRoute; +} + +// Make sure we get a cache miss with a different userContextId. +function doTest10() { + dump("doTest10()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + originAttributes = { + userContextId: 2, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest11; + do_test_pending(); + doTest(); +} + +// Make sure we get a cache miss with a different firstPartyDomain. +function doTest11() { + dump("doTest11()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + originAttributes = { + userContextId: 1, + firstPartyDomain: "b.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest12; + do_test_pending(); + doTest(); +} +// +// Make sure we get a cache hit with the same origin attributes. +function doTest12() { + dump("doTest12()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest13; + do_test_pending(); + doTest(); + // This ensures a cache hit. + xaltsvc = h3FooRoute; +} + +// Make sure we do not use H3 if it is disabled on a channel. +function doTest13() { + dump("doTest13()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + disallowH3 = true; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest14; + do_test_pending(); + doTest(); +} + +// Make sure we use H3 if only Http2 is disabled on a channel. +function doTest14() { + dump("doTest14()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + disallowH2 = true; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest15; + do_test_pending(); + doTest(); + // This should ensures a cache hit. + xaltsvc = h3FooRoute; +} + +// Make sure we do not use H3 if NS_HTTP_ALLOW_KEEPALIVE is not set. +function doTest15() { + dump("doTest15()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + testKeepAliveNotSet = true; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest16; + do_test_pending(); + doTest(); +} + +// Check we don't connect to blocked ports +function doTest16() { + dump("doTest16()\n"); + origin = httpFooOrigin; + nextTest = testsDone; + otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + otherServer.init(-1, true, -1); + xaltsvc = "localhost:" + otherServer.port; + Services.prefs.setCharPref( + "network.security.ports.banned", + "" + otherServer.port + ); + dump("Blocked port: " + otherServer.port); + waitFor = 500; + otherServer.asyncListen({ + onSocketAccepted() { + Assert.ok(false, "Got connection to socket when we didn't expect it!"); + }, + onStopListening() { + // We get closed when the entire file is done, which guarantees we get the socket accept + // if we do connect to the alt-svc header + do_test_finished(); + }, + }); + do_test_pending(); + doTest(); +} diff --git a/netwerk/test/unit/test_altsvc_pref.js b/netwerk/test/unit/test_altsvc_pref.js new file mode 100644 index 0000000000..3e6d3289f4 --- /dev/null +++ b/netwerk/test/unit/test_altsvc_pref.js @@ -0,0 +1,136 @@ +"use strict"; + +let h3Port; +let h3Route; +let h3AltSvc; +let prefs; +let httpsOrigin; + +let tests = [ + // The altSvc storage may not be up imediately, therefore run test_no_altsvc_pref + // for a couple times to wait for the storage. + test_no_altsvc_pref, + test_no_altsvc_pref, + test_no_altsvc_pref, + test_altsvc_pref, + testsDone, +]; + +let current_test = 0; + +function run_next_test() { + if (current_test < tests.length) { + dump("starting test number " + current_test + "\n"); + tests[current_test](); + current_test++; + } +} + +function run_test() { + h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + h3AltSvc = ":" + h3Port; + + h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + prefs = Services.prefs; + + prefs.setBoolPref("network.http.http3.enable", true); + prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + prefs.setBoolPref("network.dns.disableIPv6", true); + + // The certificate for the http3server server is for foo.example.com and + // is signed by http2-ca.pem so add that cert to the trust list as a + // signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + httpsOrigin = "https://foo.example.com/"; + + run_next_test(); +} + +let Http3CheckListener = function () {}; + +Http3CheckListener.prototype = { + expectedRoute: "", + expectedStatus: Cr.NS_OK, + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.status, this.expectedStatus); + if (Components.isSuccessCode(this.expectedStatus)) { + Assert.equal(request.responseStatus, 200); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, this.expectedStatus); + if (Components.isSuccessCode(this.expectedStatus)) { + Assert.equal(request.responseStatus, 200); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, this.expectedRoute); + + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + } + + do_test_finished(); + }, +}; + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function test_no_altsvc_pref() { + dump("test_no_altsvc_pref"); + do_test_pending(); + + let chan = makeChan(httpsOrigin + "http3-test"); + let listener = new Http3CheckListener(); + listener.expectedStatus = Cr.NS_ERROR_CONNECTION_REFUSED; + chan.asyncOpen(listener); +} + +function test_altsvc_pref() { + dump("test_altsvc_pref"); + do_test_pending(); + + prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "foo.example.com;h3-29=" + h3AltSvc + ); + + let chan = makeChan(httpsOrigin + "http3-test"); + let listener = new Http3CheckListener(); + listener.expectedRoute = h3Route; + chan.asyncOpen(listener); +} + +function testsDone() { + prefs.clearUserPref("network.http.http3.enable"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.dns.disableIPv6"); + prefs.clearUserPref("network.http.http3.alt-svc-mapping-for-testing"); + dump("testDone\n"); +} diff --git a/netwerk/test/unit/test_anonymous-coalescing.js b/netwerk/test/unit/test_anonymous-coalescing.js new file mode 100644 index 0000000000..c46dbfe52b --- /dev/null +++ b/netwerk/test/unit/test_anonymous-coalescing.js @@ -0,0 +1,179 @@ +/* +- test to check we use only a single connection for both onymous and anonymous requests over an existing h2 session +- request from a domain w/o LOAD_ANONYMOUS flag +- request again from the same domain, but different URI, with LOAD_ANONYMOUS flag, check the client is using the same conn +- close all and do it in the opposite way (do an anonymous req first) +*/ + +"use strict"; + +var h2Port; +var prefs; +var http2pref; +var extpref; + +function run_test() { + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Services.prefs; + + http2pref = prefs.getBoolPref("network.http.http2.enabled"); + extpref = prefs.getBoolPref("network.http.originextension"); + + prefs.setBoolPref("network.http.http2.enabled", true); + prefs.setBoolPref("network.http.originextension", true); + prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, alt1.example.com" + ); + + // The moz-http2 cert is for {foo, alt1, alt2}.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + doTest1(); +} + +function resetPrefs() { + prefs.setBoolPref("network.http.http2.enabled", http2pref); + prefs.setBoolPref("network.http.originextension", extpref); + prefs.clearUserPref("network.dns.localDomains"); +} + +function makeChan(origin) { + return NetUtil.newChannel({ + uri: origin, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var nextTest; +var origin; +var nextPortExpectedToBeSame = false; +var currentPort = 0; +var forceReload = false; +var anonymous = false; + +var Listener = function () {}; +Listener.prototype.clientPort = 0; +Listener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code! (" + request.status + ")"); + } + Assert.equal(request.responseStatus, 200); + this.clientPort = parseInt(request.getResponseHeader("x-client-port")); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.ok(Components.isSuccessCode(status)); + if (nextPortExpectedToBeSame) { + Assert.equal(currentPort, this.clientPort); + } else { + Assert.notEqual(currentPort, this.clientPort); + } + currentPort = this.clientPort; + nextTest(); + do_test_finished(); + }, +}; + +function testsDone() { + dump("testsDone\n"); + resetPrefs(); +} + +function doTest() { + dump("execute doTest " + origin + "\n"); + + var loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + if (anonymous) { + loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS; + } + anonymous = false; + if (forceReload) { + loadFlags |= Ci.nsIRequest.LOAD_FRESH_CONNECTION; + } + forceReload = false; + + var chan = makeChan(origin); + chan.loadFlags = loadFlags; + + var listener = new Listener(); + chan.asyncOpen(listener); +} + +function doTest1() { + dump("doTest1()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-1"; + nextTest = doTest2; + nextPortExpectedToBeSame = false; + do_test_pending(); + doTest(); +} + +function doTest2() { + // Run the same test as above to make sure connection is marked experienced. + dump("doTest2()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-1"; + nextTest = doTest3; + nextPortExpectedToBeSame = true; + do_test_pending(); + doTest(); +} + +function doTest3() { + // connection expected to be reused for an anonymous request + dump("doTest3()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-2"; + nextTest = doTest4; + nextPortExpectedToBeSame = true; + anonymous = true; + do_test_pending(); + doTest(); +} + +function doTest4() { + dump("doTest4()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-3"; + nextTest = doTest5; + nextPortExpectedToBeSame = false; + forceReload = true; + anonymous = true; + do_test_pending(); + doTest(); +} + +function doTest5() { + // Run the same test as above just without forceReload to make sure connection + // is marked experienced. + dump("doTest5()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-3"; + nextTest = doTest6; + nextPortExpectedToBeSame = true; + anonymous = true; + do_test_pending(); + doTest(); +} + +function doTest6() { + dump("doTest6()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-4"; + nextTest = testsDone; + nextPortExpectedToBeSame = true; + do_test_pending(); + doTest(); +} diff --git a/netwerk/test/unit/test_auth_dialog_permission.js b/netwerk/test/unit/test_auth_dialog_permission.js new file mode 100644 index 0000000000..cf7d84e339 --- /dev/null +++ b/netwerk/test/unit/test_auth_dialog_permission.js @@ -0,0 +1,278 @@ +// This file tests authentication prompt depending on pref +// network.auth.subresource-http-auth-allow: +// 0 - don't allow sub-resources to open HTTP authentication credentials +// dialogs +// 1 - allow sub-resources to open HTTP authentication credentials dialogs, +// but don't allow it for cross-origin sub-resources +// 2 - allow the cross-origin authentication as well. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var prefs = Services.prefs; + +// Since this test creates a TYPE_DOCUMENT channel via javascript, it will +// end up using the wrong LoadInfo constructor. Setting this pref will disable +// the ContentPolicyType assertion in the constructor. +prefs.setBoolPref("network.loadinfo.skip_type_assertion", true); + +function authHandler(metadata, response) { + // btoa("guest:guest"), but that function is not available here + var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; + + var body; + if ( + metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader + ) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + response.setHeader("Content-Type", "text/javascript", false); + + body = "success"; + } else { + // didn't know guest:guest, failure + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + response.setHeader("Content-Type", "text/javascript", false); + + body = "failed"; + } + + response.bodyOutputStream.write(body, body.length); +} + +var httpserv = new HttpServer(); +httpserv.registerPathHandler("/auth", authHandler); +httpserv.start(-1); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +function AuthPrompt(promptExpected) { + this.promptExpected = promptExpected; +} + +AuthPrompt.prototype = { + user: "guest", + pass: "guest", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]), + + prompt(title, text, realm, save, defaultText, result) { + do_throw("unexpected prompt call"); + }, + + promptUsernameAndPassword(title, text, realm, savePW, user, pw) { + Assert.ok(this.promptExpected, "Not expected the authentication prompt."); + + user.value = this.user; + pw.value = this.pass; + return true; + }, + + promptPassword(title, text, realm, save, pwd) { + do_throw("unexpected promptPassword call"); + }, +}; + +function Requestor(promptExpected) { + this.promptExpected = promptExpected; +} + +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface(iid) { + if (iid.equals(Ci.nsIAuthPrompt)) { + this.prompter = new AuthPrompt(this.promptExpected); + return this.prompter; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompter: null, +}; + +function make_uri(url) { + return Services.io.newURI(url); +} + +function makeChan(loadingUrl, url, contentPolicy) { + var uri = make_uri(loadingUrl); + var principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, + contentPolicyType: contentPolicy, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function Test( + subresource_http_auth_allow_pref, + loadingUri, + uri, + contentPolicy, + expectedCode +) { + this._subresource_http_auth_allow_pref = subresource_http_auth_allow_pref; + this._loadingUri = loadingUri; + this._uri = uri; + this._contentPolicy = contentPolicy; + this._expectedCode = expectedCode; +} + +Test.prototype = { + _subresource_http_auth_allow_pref: 1, + _loadingUri: null, + _uri: null, + _contentPolicy: Ci.nsIContentPolicy.TYPE_OTHER, + _expectedCode: 200, + + onStartRequest(request) { + try { + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code!"); + } + + if (!(request instanceof Ci.nsIHttpChannel)) { + do_throw("Expecting an HTTP channel"); + } + + Assert.equal(request.responseStatus, this._expectedCode); + // The request should be succeeded iff we expect 200 + Assert.equal(request.requestSucceeded, this._expectedCode == 200); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable(request, stream, offset, count) { + do_throw("Should not get any data!"); + }, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_ERROR_ABORT); + + // Clear the auth cache. + Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager) + .clearAll(); + + do_timeout(0, run_next_test); + }, + + run() { + dump( + "Run test: " + + this._subresource_http_auth_allow_pref + + this._loadingUri + + this._uri + + this._contentPolicy + + this._expectedCode + + " \n" + ); + + prefs.setIntPref( + "network.auth.subresource-http-auth-allow", + this._subresource_http_auth_allow_pref + ); + let chan = makeChan(this._loadingUri, this._uri, this._contentPolicy); + chan.notificationCallbacks = new Requestor(this._expectedCode == 200); + chan.asyncOpen(this); + }, +}; + +var tests = [ + // For the next 3 tests the preference is set to 2 - allow the cross-origin + // authentication as well. + + // A cross-origin request. + new Test( + 2, + "http://example.com", + URL + "/auth", + Ci.nsIContentPolicy.TYPE_OTHER, + 200 + ), + // A non cross-origin sub-resource request. + new Test(2, URL + "/", URL + "/auth", Ci.nsIContentPolicy.TYPE_OTHER, 200), + // A top level document. + new Test( + 2, + URL + "/auth", + URL + "/auth", + Ci.nsIContentPolicy.TYPE_DOCUMENT, + 200 + ), + + // For the next 3 tests the preference is set to 1 - allow sub-resources to + // open HTTP authentication credentials dialogs, but don't allow it for + // cross-origin sub-resources + + // A cross-origin request. + new Test( + 1, + "http://example.com", + URL + "/auth", + Ci.nsIContentPolicy.TYPE_OTHER, + 401 + ), + // A non cross-origin sub-resource request. + new Test(1, URL + "/", URL + "/auth", Ci.nsIContentPolicy.TYPE_OTHER, 200), + // A top level document. + new Test( + 1, + URL + "/auth", + URL + "/auth", + Ci.nsIContentPolicy.TYPE_DOCUMENT, + 200 + ), + + // For the next 3 tests the preference is set to 0 - don't allow sub-resources + // to open HTTP authentication credentials dialogs. + + // A cross-origin request. + new Test( + 0, + "http://example.com", + URL + "/auth", + Ci.nsIContentPolicy.TYPE_OTHER, + 401 + ), + // A sub-resource request. + new Test(0, URL + "/", URL + "/auth", Ci.nsIContentPolicy.TYPE_OTHER, 401), + // A top level request. + new Test( + 0, + URL + "/auth", + URL + "/auth", + Ci.nsIContentPolicy.TYPE_DOCUMENT, + 200 + ), +]; + +function run_next_test() { + var nextTest = tests.shift(); + if (!nextTest) { + httpserv.stop(do_test_finished); + return; + } + + nextTest.run(); +} + +function run_test() { + do_test_pending(); + run_next_test(); +} diff --git a/netwerk/test/unit/test_auth_jar.js b/netwerk/test/unit/test_auth_jar.js new file mode 100644 index 0000000000..a6f1ea257c --- /dev/null +++ b/netwerk/test/unit/test_auth_jar.js @@ -0,0 +1,92 @@ +"use strict"; + +function createURI(s) { + return Services.io.newURI(s); +} + +function run_test() { + // Set up a profile. + do_get_profile(); + + var secMan = Services.scriptSecurityManager; + const kURI1 = "http://example.com"; + var app = secMan.createContentPrincipal(createURI(kURI1), {}); + var appbrowser = secMan.createContentPrincipal(createURI(kURI1), { + inIsolatedMozBrowser: true, + }); + + var am = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + am.setAuthIdentity( + "http", + "a.example.com", + -1, + "basic", + "realm", + "", + "example.com", + "user", + "pass", + false, + app + ); + am.setAuthIdentity( + "http", + "a.example.com", + -1, + "basic", + "realm", + "", + "example.com", + "user3", + "pass3", + false, + appbrowser + ); + + Services.clearData.deleteDataFromOriginAttributesPattern({ + inIsolatedMozBrowser: true, + }); + + var domain = { value: "" }, + user = { value: "" }, + pass = { value: "" }; + try { + am.getAuthIdentity( + "http", + "a.example.com", + -1, + "basic", + "realm", + "", + domain, + user, + pass, + false, + appbrowser + ); + Assert.equal(false, true); // no identity should be present + } catch (x) { + Assert.equal(domain.value, ""); + Assert.equal(user.value, ""); + Assert.equal(pass.value, ""); + } + + am.getAuthIdentity( + "http", + "a.example.com", + -1, + "basic", + "realm", + "", + domain, + user, + pass, + false, + app + ); + Assert.equal(domain.value, "example.com"); + Assert.equal(user.value, "user"); + Assert.equal(pass.value, "pass"); +} diff --git a/netwerk/test/unit/test_auth_multiple.js b/netwerk/test/unit/test_auth_multiple.js new file mode 100644 index 0000000000..8186e8080d --- /dev/null +++ b/netwerk/test/unit/test_auth_multiple.js @@ -0,0 +1,462 @@ +// This file tests authentication prompt callbacks +// TODO NIT use do_check_eq(expected, actual) consistently, not sometimes eq(actual, expected) + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// Turn off the authentication dialog blocking for this test. +var prefs = Services.prefs; +prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + +function URL(domain, path = "") { + if (path.startsWith("/")) { + path = path.substring(1); + } + return `http://${domain}:${httpserv.identity.primaryPort}/${path}`; +} + +XPCOMUtils.defineLazyGetter(this, "PORT", function () { + return httpserv.identity.primaryPort; +}); + +const FLAG_RETURN_FALSE = 1 << 0; +const FLAG_WRONG_PASSWORD = 1 << 1; +const FLAG_BOGUS_USER = 1 << 2; +// const FLAG_PREVIOUS_FAILED = 1 << 3; +const CROSS_ORIGIN = 1 << 4; +// const FLAG_NO_REALM = 1 << 5; +const FLAG_NON_ASCII_USER_PASSWORD = 1 << 6; + +function AuthPrompt1(flags) { + this.flags = flags; +} + +AuthPrompt1.prototype = { + user: "guest", + pass: "guest", + + expectedRealm: "secret", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]), + + prompt: function ap1_prompt(title, text, realm, save, defaultText, result) { + do_throw("unexpected prompt call"); + }, + + promptUsernameAndPassword: function ap1_promptUP( + title, + text, + realm, + savePW, + user, + pw + ) { + if (!(this.flags & CROSS_ORIGIN)) { + if (!text.includes(this.expectedRealm)) { + do_throw("Text must indicate the realm"); + } + } else if (text.includes(this.expectedRealm)) { + do_throw("There should not be realm for cross origin"); + } + if (!text.includes("localhost")) { + do_throw("Text must indicate the hostname"); + } + if (!text.includes(String(PORT))) { + do_throw("Text must indicate the port"); + } + if (text.includes("-1")) { + do_throw("Text must contain negative numbers"); + } + + if (this.flags & FLAG_RETURN_FALSE) { + return false; + } + + if (this.flags & FLAG_BOGUS_USER) { + this.user = "foo\nbar"; + } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { + this.user = "é"; + } + + user.value = this.user; + if (this.flags & FLAG_WRONG_PASSWORD) { + pw.value = this.pass + ".wrong"; + // Now clear the flag to avoid an infinite loop + this.flags &= ~FLAG_WRONG_PASSWORD; + } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { + pw.value = "é"; + } else { + pw.value = this.pass; + } + return true; + }, + + promptPassword: function ap1_promptPW(title, text, realm, save, pwd) { + do_throw("unexpected promptPassword call"); + }, +}; + +function AuthPrompt2(flags) { + this.flags = flags; +} + +AuthPrompt2.prototype = { + user: "guest", + pass: "guest", + + expectedRealm: "secret", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + + promptAuth: function ap2_promptAuth(channel, level, authInfo) { + authInfo.username = this.user; + authInfo.password = this.pass; + return true; + }, + + asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +function Requestor(flags, versions) { + this.flags = flags; + this.versions = versions; +} + +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface: function requestor_gi(iid) { + if (this.versions & 1 && iid.equals(Ci.nsIAuthPrompt)) { + // Allow the prompt to store state by caching it here + if (!this.prompt1) { + this.prompt1 = new AuthPrompt1(this.flags); + } + return this.prompt1; + } + if (this.versions & 2 && iid.equals(Ci.nsIAuthPrompt2)) { + // Allow the prompt to store state by caching it here + if (!this.prompt2) { + this.prompt2 = new AuthPrompt2(this.flags); + } + return this.prompt2; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompt1: null, + prompt2: null, +}; + +function RealmTestRequestor() {} + +RealmTestRequestor.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIAuthPrompt2", + ]), + + getInterface: function realmtest_interface(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + return this; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + promptAuth: function realmtest_checkAuth(channel, level, authInfo) { + Assert.equal(authInfo.realm, '"foo_bar'); + + return false; + }, + + asyncPromptAuth: function realmtest_async(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +function makeChan(url) { + let loadingUrl = Services.io + .newURI(url) + .mutate() + .setPathQueryRef("") + .finalize(); + var principal = Services.scriptSecurityManager.createContentPrincipal( + loadingUrl, + {} + ); + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); +} + +function ntlm_auth(metadata, response) { + let challenge = metadata.getHeader("Authorization"); + if (!challenge.startsWith("NTLM ")) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + return; + } + + let decoded = atob(challenge.substring(5)); + info(decoded); + + if (!decoded.startsWith("NTLMSSP\0")) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + return; + } + + let isNegotiate = decoded.substring(8).startsWith("\x01\x00\x00\x00"); + let isAuthenticate = decoded.substring(8).startsWith("\x03\x00\x00\x00"); + + if (isNegotiate) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader( + "WWW-Authenticate", + "NTLM TlRMTVNTUAACAAAAAAAAAAAoAAABggAAASNFZ4mrze8AAAAAAAAAAAAAAAAAAAAA", + false + ); + return; + } + + if (isAuthenticate) { + let body = "OK"; + response.bodyOutputStream.write(body, body.length); + return; + } + + // Something else went wrong. + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); +} + +function basic_auth(metadata, response) { + let challenge = metadata.getHeader("Authorization"); + if (!challenge.startsWith("Basic ")) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + return; + } + + if (challenge == "Basic Z3Vlc3Q6Z3Vlc3Q=") { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + let body = "success"; + response.bodyOutputStream.write(body, body.length); + return; + } + + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); +} + +// +// Digest functions +// +function bytesFromString(str) { + const encoder = new TextEncoder(); + return encoder.encode(str); +} + +// return the two-digit hexadecimal code for a byte +function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); +} + +function H(str) { + var data = bytesFromString(str); + var ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + ch.init(Ci.nsICryptoHash.MD5); + ch.update(data, data.length); + var hash = ch.finish(false); + return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join(""); +} + +const nonce = "6f93719059cf8d568005727f3250e798"; +const opaque = "1234opaque1234"; +const digestChallenge = `Digest realm="secret", domain="/", qop=auth,algorithm=MD5, nonce="${nonce}" opaque="${opaque}"`; +// +// Digest handler +// +// /auth/digest +function authDigest(metadata, response) { + var cnonceRE = /cnonce="(\w+)"/; + var responseRE = /response="(\w+)"/; + var usernameRE = /username="(\w+)"/; + var body = ""; + // check creds if we have them + if (metadata.hasHeader("Authorization")) { + var auth = metadata.getHeader("Authorization"); + var cnonce = auth.match(cnonceRE)[1]; + var clientDigest = auth.match(responseRE)[1]; + var username = auth.match(usernameRE)[1]; + var nc = "00000001"; + + if (username != "guest") { + response.setStatusLine(metadata.httpVersion, 400, "bad request"); + body = "should never get here"; + } else { + // see RFC2617 for the description of this calculation + var A1 = "guest:secret:guest"; + var A2 = "GET:/path"; + var noncebits = [nonce, nc, cnonce, "auth", H(A2)].join(":"); + var digest = H([H(A1), noncebits].join(":")); + + if (clientDigest == digest) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + body = "digest"; + } else { + info(clientDigest); + info(digest); + handle_unauthorized(metadata, response); + return; + } + } + } else { + // no header, send one + handle_unauthorized(metadata, response); + return; + } + + response.bodyOutputStream.write(body, body.length); +} + +let challenges = ["NTLM", `Basic realm="secret"`, digestChallenge]; + +function handle_unauthorized(metadata, response) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + + for (let ch of challenges) { + response.setHeader("WWW-Authenticate", ch, true); + } +} + +// /path +function auth_handler(metadata, response) { + if (!metadata.hasHeader("Authorization")) { + handle_unauthorized(metadata, response); + return; + } + + let challenge = metadata.getHeader("Authorization"); + if (challenge.startsWith("NTLM ")) { + ntlm_auth(metadata, response); + return; + } + + if (challenge.startsWith("Basic ")) { + basic_auth(metadata, response); + return; + } + + if (challenge.startsWith("Digest ")) { + authDigest(metadata, response); + return; + } + + handle_unauthorized(metadata, response); +} + +let httpserv; +add_setup(() => { + Services.prefs.setBoolPref("network.auth.force-generic-ntlm", true); + Services.prefs.setBoolPref("network.auth.force-generic-ntlm-v1", true); + Services.prefs.setBoolPref("network.dns.native-is-localhost", true); + Services.prefs.setBoolPref("network.http.sanitize-headers-in-logs", false); + + httpserv = new HttpServer(); + httpserv.registerPathHandler("/path", auth_handler); + httpserv.start(-1); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.auth.force-generic-ntlm"); + Services.prefs.clearUserPref("network.auth.force-generic-ntlm-v1"); + Services.prefs.clearUserPref("network.dns.native-is-localhost"); + Services.prefs.clearUserPref("network.http.sanitize-headers-in-logs"); + + await httpserv.stop(); + }); +}); + +add_task(async function test_ntlm_first() { + Services.prefs.setBoolPref( + "network.auth.choose_most_secure_challenge", + false + ); + challenges = ["NTLM", `Basic realm="secret"`, digestChallenge]; + httpserv.identity.add("http", "ntlm.com", httpserv.identity.primaryPort); + let chan = makeChan(URL("ntlm.com", "/path")); + + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); + let [req, buf] = await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener((req, buf) => resolve([req, buf]), null) + ); + }); + Assert.equal(buf, "OK"); + Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); +}); + +add_task(async function test_basic_first() { + Services.prefs.setBoolPref( + "network.auth.choose_most_secure_challenge", + false + ); + challenges = [`Basic realm="secret"`, "NTLM", digestChallenge]; + httpserv.identity.add("http", "basic.com", httpserv.identity.primaryPort); + let chan = makeChan(URL("basic.com", "/path")); + + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); + let [req, buf] = await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener((req, buf) => resolve([req, buf]), null) + ); + }); + Assert.equal(buf, "success"); + Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); +}); + +add_task(async function test_digest_first() { + Services.prefs.setBoolPref( + "network.auth.choose_most_secure_challenge", + false + ); + challenges = [digestChallenge, `Basic realm="secret"`, "NTLM"]; + httpserv.identity.add("http", "digest.com", httpserv.identity.primaryPort); + let chan = makeChan(URL("digest.com", "/path")); + + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); + let [req, buf] = await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener((req, buf) => resolve([req, buf]), null) + ); + }); + Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + Assert.equal(buf, "digest"); +}); + +add_task(async function test_choose_most_secure() { + // When the pref is true, we rank the challenges by how secure they are. + // In this case, NTLM should be the most secure. + Services.prefs.setBoolPref("network.auth.choose_most_secure_challenge", true); + challenges = [digestChallenge, `Basic realm="secret"`, "NTLM"]; + httpserv.identity.add( + "http", + "ntlmstrong.com", + httpserv.identity.primaryPort + ); + let chan = makeChan(URL("ntlmstrong.com", "/path")); + + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); + let [req, buf] = await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener((req, buf) => resolve([req, buf]), null) + ); + }); + Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + Assert.equal(buf, "OK"); +}); diff --git a/netwerk/test/unit/test_auth_proxy.js b/netwerk/test/unit/test_auth_proxy.js new file mode 100644 index 0000000000..35201961ec --- /dev/null +++ b/netwerk/test/unit/test_auth_proxy.js @@ -0,0 +1,461 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 tests the automatic login to the proxy with password, + * if the password is stored and the browser is restarted. + * + * <copied from="test_authentication.js"/> + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const FLAG_RETURN_FALSE = 1 << 0; +const FLAG_WRONG_PASSWORD = 1 << 1; +const FLAG_PREVIOUS_FAILED = 1 << 2; + +function AuthPrompt2(proxyFlags, hostFlags) { + this.proxyCred.flags = proxyFlags; + this.hostCred.flags = hostFlags; +} +AuthPrompt2.prototype = { + proxyCred: { + user: "proxy", + pass: "guest", + realmExpected: "intern", + flags: 0, + }, + hostCred: { user: "host", pass: "guest", realmExpected: "extern", flags: 0 }, + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + + promptAuth: function ap2_promptAuth(channel, encryptionLevel, authInfo) { + try { + // never HOST and PROXY set at the same time in prompt + Assert.equal( + (authInfo.flags & Ci.nsIAuthInformation.AUTH_HOST) != 0, + (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) == 0 + ); + + var isProxy = (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) != 0; + var cred = isProxy ? this.proxyCred : this.hostCred; + + dump( + "with flags: " + + ((cred.flags & FLAG_WRONG_PASSWORD) != 0 ? "wrong password" : "") + + " " + + ((cred.flags & FLAG_PREVIOUS_FAILED) != 0 ? "previous failed" : "") + + " " + + ((cred.flags & FLAG_RETURN_FALSE) != 0 ? "return false" : "") + + "\n" + ); + + // PROXY properly set by necko (checked using realm) + Assert.equal(cred.realmExpected, authInfo.realm); + + // PREVIOUS_FAILED properly set by necko + Assert.equal( + (cred.flags & FLAG_PREVIOUS_FAILED) != 0, + (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) != 0 + ); + + if (cred.flags & FLAG_RETURN_FALSE) { + cred.flags |= FLAG_PREVIOUS_FAILED; + cred.flags &= ~FLAG_RETURN_FALSE; + return false; + } + + authInfo.username = cred.user; + if (cred.flags & FLAG_WRONG_PASSWORD) { + authInfo.password = cred.pass + ".wrong"; + cred.flags |= FLAG_PREVIOUS_FAILED; + // Now clear the flag to avoid an infinite loop + cred.flags &= ~FLAG_WRONG_PASSWORD; + } else { + authInfo.password = cred.pass; + cred.flags &= ~FLAG_PREVIOUS_FAILED; + } + } catch (e) { + do_throw(e); + } + return true; + }, + + asyncPromptAuth: function ap2_async( + channel, + callback, + context, + encryptionLevel, + authInfo + ) { + var me = this; + var allOverAndDead = false; + executeSoon(function () { + try { + if (allOverAndDead) { + throw new Error("already canceled"); + } + var ret = me.promptAuth(channel, encryptionLevel, authInfo); + if (!ret) { + callback.onAuthCancelled(context, true); + } else { + callback.onAuthAvailable(context, authInfo); + } + allOverAndDead = true; + } catch (e) { + do_throw(e); + } + }); + return new Cancelable(function () { + if (allOverAndDead) { + throw new Error("can't cancel, already ran"); + } + callback.onAuthAvailable(context, authInfo); + allOverAndDead = true; + }); + }, +}; + +function Cancelable(onCancelFunc) { + this.onCancelFunc = onCancelFunc; +} +Cancelable.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsICancelable"]), + cancel: function cancel() { + try { + this.onCancelFunc(); + } catch (e) { + do_throw(e); + } + }, +}; + +function Requestor(proxyFlags, hostFlags) { + this.proxyFlags = proxyFlags; + this.hostFlags = hostFlags; +} +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface: function requestor_gi(iid) { + if (iid.equals(Ci.nsIAuthPrompt)) { + dump("authprompt1 not implemented\n"); + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } + if (iid.equals(Ci.nsIAuthPrompt2)) { + try { + // Allow the prompt to store state by caching it here + if (!this.prompt2) { + this.prompt2 = new AuthPrompt2(this.proxyFlags, this.hostFlags); + } + return this.prompt2; + } catch (e) { + do_throw(e); + } + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompt2: null, +}; + +var listener = { + expectedCode: -1, // uninitialized + + onStartRequest: function test_onStartR(request) { + try { + // Proxy auth cancellation return failures to avoid spoofing + if ( + !Components.isSuccessCode(request.status) && + this.expectedCode != 407 + ) { + do_throw("Channel should have a success code!"); + } + + if (!(request instanceof Ci.nsIHttpChannel)) { + do_throw("Expecting an HTTP channel"); + } + + Assert.equal(this.expectedCode, request.responseStatus); + // If we expect 200, the request should have succeeded + Assert.equal(this.expectedCode == 200, request.requestSucceeded); + + var cookie = ""; + try { + cookie = request.getRequestHeader("Cookie"); + } catch (e) {} + Assert.equal(cookie, ""); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + Assert.equal(status, Cr.NS_ERROR_ABORT); + + if (current_test < tests.length - 1) { + // First, need to clear the auth cache + Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager) + .clearAll(); + + current_test++; + tests[current_test](); + } else { + do_test_pending(); + httpserv.stop(do_test_finished); + } + + do_test_finished(); + }, +}; + +function makeChan(url) { + if (!url) { + url = "http://somesite/"; + } + + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var current_test = 0; +var httpserv = null; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/", proxyAuthHandler); + httpserv.identity.add("http", "somesite", 80); + httpserv.start(-1); + + Services.prefs.setCharPref("network.proxy.http", "localhost"); + Services.prefs.setIntPref( + "network.proxy.http_port", + httpserv.identity.primaryPort + ); + Services.prefs.setCharPref("network.proxy.no_proxies_on", ""); + Services.prefs.setIntPref("network.proxy.type", 1); + + // Turn off the authentication dialog blocking for this test. + Services.prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + Services.prefs.setBoolPref( + "network.auth.non-web-content-triggered-resources-http-auth-allow", + true + ); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.proxy.http"); + Services.prefs.clearUserPref("network.proxy.http_port"); + Services.prefs.clearUserPref("network.proxy.no_proxies_on"); + Services.prefs.clearUserPref("network.proxy.type"); + Services.prefs.clearUserPref("network.auth.subresource-http-auth-allow"); + Services.prefs.clearUserPref( + "network.auth.non-web-content-triggered-resources-http-auth-allow" + ); + }); + + tests[current_test](); +} + +function test_proxy_returnfalse() { + dump("\ntest: proxy returnfalse\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 0); + listener.expectedCode = 407; // Proxy Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_proxy_wrongpw() { + dump("\ntest: proxy wrongpw\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(FLAG_WRONG_PASSWORD, 0); + listener.expectedCode = 200; // Eventually OK + chan.asyncOpen(listener); + do_test_pending(); +} + +function test_all_ok() { + dump("\ntest: all ok\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(0, 0); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + do_test_pending(); +} + +function test_proxy_407_cookie() { + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 0); + chan.setRequestHeader("X-Set-407-Cookie", "1", false); + listener.expectedCode = 407; // Proxy Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_proxy_200_cookie() { + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(0, 0); + chan.setRequestHeader("X-Set-407-Cookie", "1", false); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + do_test_pending(); +} + +function test_host_returnfalse() { + dump("\ntest: host returnfalse\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(0, FLAG_RETURN_FALSE); + listener.expectedCode = 401; // Host Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_host_wrongpw() { + dump("\ntest: host wrongpw\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(0, FLAG_WRONG_PASSWORD); + listener.expectedCode = 200; // Eventually OK + chan.asyncOpen(listener); + do_test_pending(); +} + +function test_proxy_wrongpw_host_wrongpw() { + dump("\ntest: proxy wrongpw, host wrongpw\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor( + FLAG_WRONG_PASSWORD, + FLAG_WRONG_PASSWORD + ); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + do_test_pending(); +} + +function test_proxy_wrongpw_host_returnfalse() { + dump("\ntest: proxy wrongpw, host return false\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor( + FLAG_WRONG_PASSWORD, + FLAG_RETURN_FALSE + ); + listener.expectedCode = 401; // Host Unauthorized + chan.asyncOpen(listener); + do_test_pending(); +} + +var tests = [ + test_proxy_returnfalse, + test_proxy_wrongpw, + test_all_ok, + test_proxy_407_cookie, + test_proxy_200_cookie, + test_host_returnfalse, + test_host_wrongpw, + test_proxy_wrongpw_host_wrongpw, + test_proxy_wrongpw_host_returnfalse, +]; + +// PATH HANDLERS + +// Proxy +function proxyAuthHandler(metadata, response) { + try { + var realm = "intern"; + // btoa("proxy:guest"), but that function is not available here + var expectedHeader = "Basic cHJveHk6Z3Vlc3Q="; + + var body; + if ( + metadata.hasHeader("Proxy-Authorization") && + metadata.getHeader("Proxy-Authorization") == expectedHeader + ) { + dump("proxy password ok\n"); + response.setHeader( + "Proxy-Authenticate", + 'Basic realm="' + realm + '"', + false + ); + + hostAuthHandler(metadata, response); + } else { + dump("proxy password required\n"); + response.setStatusLine( + metadata.httpVersion, + 407, + "Unauthorized by HTTP proxy" + ); + response.setHeader( + "Proxy-Authenticate", + 'Basic realm="' + realm + '"', + false + ); + if (metadata.hasHeader("X-Set-407-Cookie")) { + response.setHeader("Set-Cookie", "chewy", false); + } + body = "failed"; + response.bodyOutputStream.write(body, body.length); + } + } catch (e) { + do_throw(e); + } +} + +// Host /auth +function hostAuthHandler(metadata, response) { + try { + var realm = "extern"; + // btoa("host:guest"), but that function is not available here + var expectedHeader = "Basic aG9zdDpndWVzdA=="; + + var body; + if ( + metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader + ) { + dump("host password ok\n"); + response.setStatusLine( + metadata.httpVersion, + 200, + "OK, authorized for host" + ); + response.setHeader( + "WWW-Authenticate", + 'Basic realm="' + realm + '"', + false + ); + body = "success"; + } else { + dump("host password required\n"); + response.setStatusLine( + metadata.httpVersion, + 401, + "Unauthorized by HTTP server host" + ); + response.setHeader( + "WWW-Authenticate", + 'Basic realm="' + realm + '"', + false + ); + body = "failed"; + } + response.bodyOutputStream.write(body, body.length); + } catch (e) { + do_throw(e); + } +} diff --git a/netwerk/test/unit/test_authentication.js b/netwerk/test/unit/test_authentication.js new file mode 100644 index 0000000000..1e9617178c --- /dev/null +++ b/netwerk/test/unit/test_authentication.js @@ -0,0 +1,1233 @@ +// This file tests authentication prompt callbacks +// TODO NIT use do_check_eq(expected, actual) consistently, not sometimes eq(actual, expected) + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// Turn off the authentication dialog blocking for this test. +Services.prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "PORT", function () { + return httpserv.identity.primaryPort; +}); + +const FLAG_RETURN_FALSE = 1 << 0; +const FLAG_WRONG_PASSWORD = 1 << 1; +const FLAG_BOGUS_USER = 1 << 2; +const FLAG_PREVIOUS_FAILED = 1 << 3; +const CROSS_ORIGIN = 1 << 4; +const FLAG_NO_REALM = 1 << 5; +const FLAG_NON_ASCII_USER_PASSWORD = 1 << 6; + +const nsIAuthPrompt2 = Ci.nsIAuthPrompt2; +const nsIAuthInformation = Ci.nsIAuthInformation; + +function AuthPrompt1(flags) { + this.flags = flags; +} + +AuthPrompt1.prototype = { + user: "guest", + pass: "guest", + + expectedRealm: "secret", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]), + + prompt: function ap1_prompt(title, text, realm, save, defaultText, result) { + do_throw("unexpected prompt call"); + }, + + promptUsernameAndPassword: function ap1_promptUP( + title, + text, + realm, + savePW, + user, + pw + ) { + if (this.flags & FLAG_NO_REALM) { + // Note that the realm here isn't actually the realm. it's a pw mgr key. + Assert.equal(URL + " (" + this.expectedRealm + ")", realm); + } + if (!(this.flags & CROSS_ORIGIN)) { + if (!text.includes(this.expectedRealm)) { + do_throw("Text must indicate the realm"); + } + } else if (text.includes(this.expectedRealm)) { + do_throw("There should not be realm for cross origin"); + } + if (!text.includes("localhost")) { + do_throw("Text must indicate the hostname"); + } + if (!text.includes(String(PORT))) { + do_throw("Text must indicate the port"); + } + if (text.includes("-1")) { + do_throw("Text must contain negative numbers"); + } + + if (this.flags & FLAG_RETURN_FALSE) { + return false; + } + + if (this.flags & FLAG_BOGUS_USER) { + this.user = "foo\nbar"; + } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { + this.user = "é"; + } + + user.value = this.user; + if (this.flags & FLAG_WRONG_PASSWORD) { + pw.value = this.pass + ".wrong"; + // Now clear the flag to avoid an infinite loop + this.flags &= ~FLAG_WRONG_PASSWORD; + } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { + pw.value = "é"; + } else { + pw.value = this.pass; + } + return true; + }, + + promptPassword: function ap1_promptPW(title, text, realm, save, pwd) { + do_throw("unexpected promptPassword call"); + }, +}; + +function AuthPrompt2(flags) { + this.flags = flags; +} + +AuthPrompt2.prototype = { + user: "guest", + pass: "guest", + + expectedRealm: "secret", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + + promptAuth: function ap2_promptAuth(channel, level, authInfo) { + var isNTLM = channel.URI.pathQueryRef.includes("ntlm"); + var isDigest = channel.URI.pathQueryRef.includes("digest"); + + if (isNTLM || this.flags & FLAG_NO_REALM) { + this.expectedRealm = ""; // NTLM knows no realms + } + + Assert.equal(this.expectedRealm, authInfo.realm); + + var expectedLevel = + isNTLM || isDigest + ? nsIAuthPrompt2.LEVEL_PW_ENCRYPTED + : nsIAuthPrompt2.LEVEL_NONE; + Assert.equal(expectedLevel, level); + + var expectedFlags = nsIAuthInformation.AUTH_HOST; + + if (this.flags & FLAG_PREVIOUS_FAILED) { + expectedFlags |= nsIAuthInformation.PREVIOUS_FAILED; + } + + if (this.flags & CROSS_ORIGIN) { + expectedFlags |= nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE; + } + + if (isNTLM) { + expectedFlags |= nsIAuthInformation.NEED_DOMAIN; + } + + const kAllKnownFlags = 127; // Don't fail test for newly added flags + Assert.equal(expectedFlags, authInfo.flags & kAllKnownFlags); + + // eslint-disable-next-line no-nested-ternary + var expectedScheme = isNTLM ? "ntlm" : isDigest ? "digest" : "basic"; + Assert.equal(expectedScheme, authInfo.authenticationScheme); + + // No passwords in the URL -> nothing should be prefilled + Assert.equal(authInfo.username, ""); + Assert.equal(authInfo.password, ""); + Assert.equal(authInfo.domain, ""); + + if (this.flags & FLAG_RETURN_FALSE) { + this.flags |= FLAG_PREVIOUS_FAILED; + return false; + } + + if (this.flags & FLAG_BOGUS_USER) { + this.user = "foo\nbar"; + } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { + this.user = "é"; + } + + authInfo.username = this.user; + if (this.flags & FLAG_WRONG_PASSWORD) { + authInfo.password = this.pass + ".wrong"; + this.flags |= FLAG_PREVIOUS_FAILED; + // Now clear the flag to avoid an infinite loop + this.flags &= ~FLAG_WRONG_PASSWORD; + } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { + authInfo.password = "é"; + } else { + authInfo.password = this.pass; + this.flags &= ~FLAG_PREVIOUS_FAILED; + } + return true; + }, + + asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) { + let self = this; + executeSoon(function () { + let ret = self.promptAuth(chan, lvl, info); + if (ret) { + cb.onAuthAvailable(ctx, info); + } else { + cb.onAuthCancelled(ctx, true); + } + }); + }, +}; + +function Requestor(flags, versions) { + this.flags = flags; + this.versions = versions; +} + +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface: function requestor_gi(iid) { + if (this.versions & 1 && iid.equals(Ci.nsIAuthPrompt)) { + // Allow the prompt to store state by caching it here + if (!this.prompt1) { + this.prompt1 = new AuthPrompt1(this.flags); + } + return this.prompt1; + } + if (this.versions & 2 && iid.equals(Ci.nsIAuthPrompt2)) { + // Allow the prompt to store state by caching it here + if (!this.prompt2) { + this.prompt2 = new AuthPrompt2(this.flags); + } + return this.prompt2; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompt1: null, + prompt2: null, +}; + +function RealmTestRequestor() { + this.promptRealm = ""; +} + +RealmTestRequestor.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIAuthPrompt2", + ]), + + getInterface: function realmtest_interface(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + return this; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + promptAuth: function realmtest_checkAuth(channel, level, authInfo) { + this.promptRealm = authInfo.realm; + + return false; + }, + + asyncPromptAuth: function realmtest_async(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +var listener = { + expectedCode: -1, // Uninitialized + nextTest: undefined, + + onStartRequest: function test_onStartR(request) { + try { + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code!"); + } + + if (!(request instanceof Ci.nsIHttpChannel)) { + do_throw("Expecting an HTTP channel"); + } + + Assert.equal(request.responseStatus, this.expectedCode); + // The request should be succeeded if we expect 200 + Assert.equal(request.requestSucceeded, this.expectedCode == 200); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + Assert.equal(status, Cr.NS_ERROR_ABORT); + + this.nextTest(); + }, +}; + +function makeChan( + url, + loadingUrl, + securityFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType = Ci.nsIContentPolicy.TYPE_OTHER +) { + var principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(loadingUrl), + {} + ); + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags, + contentPolicyType, + }); +} + +var httpserv = null; + +function setup() { + httpserv = new HttpServer(); + + httpserv.registerPathHandler("/auth", authHandler); + httpserv.registerPathHandler("/auth/ntlm/simple", authNtlmSimple); + httpserv.registerPathHandler("/auth/realm", authRealm); + httpserv.registerPathHandler("/auth/non_ascii", authNonascii); + httpserv.registerPathHandler("/auth/digest_md5", authDigestMD5); + httpserv.registerPathHandler("/auth/digest_md5sess", authDigestMD5sess); + httpserv.registerPathHandler("/auth/digest_sha256", authDigestSHA256); + httpserv.registerPathHandler("/auth/digest_sha256sess", authDigestSHA256sess); + httpserv.registerPathHandler("/auth/digest_sha256_md5", authDigestSHA256_MD5); + httpserv.registerPathHandler("/auth/digest_md5_sha256", authDigestMD5_SHA256); + httpserv.registerPathHandler( + "/auth/digest_md5_sha256_oneline", + authDigestMD5_SHA256_oneline + ); + httpserv.registerPathHandler("/auth/short_digest", authShortDigest); + httpserv.registerPathHandler("/largeRealm", largeRealm); + httpserv.registerPathHandler("/largeDomain", largeDomain); + + httpserv.registerPathHandler("/corp-coep", corpAndCoep); + + httpserv.start(-1); + + registerCleanupFunction(async () => { + await httpserv.stop(); + }); +} +setup(); + +async function openAndListen(chan) { + await new Promise(resolve => { + listener.nextTest = resolve; + chan.asyncOpen(listener); + }); + Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager) + .clearAll(); +} + +add_task(async function test_noauth() { + var chan = makeChan(URL + "/auth", URL); + + listener.expectedCode = 401; // Unauthorized + await openAndListen(chan); +}); + +add_task(async function test_returnfalse1() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 1); + listener.expectedCode = 401; // Unauthorized + await openAndListen(chan); +}); + +add_task(async function test_wrongpw1() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(FLAG_WRONG_PASSWORD, 1); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_prompt1() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(0, 1); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_prompt1CrossOrigin() { + var chan = makeChan(URL + "/auth", "http://example.org"); + + chan.notificationCallbacks = new Requestor(16, 1); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_prompt2CrossOrigin() { + var chan = makeChan(URL + "/auth", "http://example.org"); + + chan.notificationCallbacks = new Requestor(16, 2); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_returnfalse2() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); + listener.expectedCode = 401; // Unauthorized + await openAndListen(chan); +}); + +add_task(async function test_wrongpw2() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(FLAG_WRONG_PASSWORD, 2); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_prompt2() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(0, 2); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_ntlm() { + var chan = makeChan(URL + "/auth/ntlm/simple", URL); + + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); + listener.expectedCode = 401; // Unauthorized + await openAndListen(chan); +}); + +add_task(async function test_basicrealm() { + var chan = makeChan(URL + "/auth/realm", URL); + + let requestor = new RealmTestRequestor(); + chan.notificationCallbacks = requestor; + listener.expectedCode = 401; // Unauthorized + await openAndListen(chan); + Assert.equal(requestor.promptRealm, '"foo_bar'); +}); + +add_task(async function test_nonascii() { + var chan = makeChan(URL + "/auth/non_ascii", URL); + + chan.notificationCallbacks = new Requestor(FLAG_NON_ASCII_USER_PASSWORD, 2); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_digest_noauth() { + var chan = makeChan(URL + "/auth/digest_md5", URL); + + // chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); + listener.expectedCode = 401; // Unauthorized + await openAndListen(chan); +}); + +add_task(async function test_digest_md5() { + var chan = makeChan(URL + "/auth/digest_md5", URL); + + chan.notificationCallbacks = new Requestor(0, 2); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_digest_md5sess() { + var chan = makeChan(URL + "/auth/digest_md5sess", URL); + + chan.notificationCallbacks = new Requestor(0, 2); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_digest_sha256() { + var chan = makeChan(URL + "/auth/digest_sha256", URL); + + chan.notificationCallbacks = new Requestor(0, 2); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_digest_sha256sess() { + var chan = makeChan(URL + "/auth/digest_sha256sess", URL); + + chan.notificationCallbacks = new Requestor(0, 2); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_digest_sha256_md5() { + var chan = makeChan(URL + "/auth/digest_sha256_md5", URL); + + chan.notificationCallbacks = new Requestor(0, 2); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_digest_md5_sha256() { + var chan = makeChan(URL + "/auth/digest_md5_sha256", URL); + + chan.notificationCallbacks = new Requestor(0, 2); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_digest_md5_sha256_oneline() { + var chan = makeChan(URL + "/auth/digest_md5_sha256_oneline", URL); + + chan.notificationCallbacks = new Requestor(0, 2); + listener.expectedCode = 200; // OK + await openAndListen(chan); +}); + +add_task(async function test_digest_bogus_user() { + var chan = makeChan(URL + "/auth/digest_md5", URL); + chan.notificationCallbacks = new Requestor(FLAG_BOGUS_USER, 2); + listener.expectedCode = 401; // unauthorized + await openAndListen(chan); +}); + +// Test header "WWW-Authenticate: Digest" - bug 1338876. +add_task(async function test_short_digest() { + var chan = makeChan(URL + "/auth/short_digest", URL); + chan.notificationCallbacks = new Requestor(FLAG_NO_REALM, 2); + listener.expectedCode = 401; // OK + await openAndListen(chan); +}); + +// Test that COOP/COEP are processed even though asyncPromptAuth is cancelled. +add_task(async function test_corp_coep() { + var chan = makeChan( + URL + "/corp-coep", + URL, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, + Ci.nsIContentPolicy.TYPE_DOCUMENT + ); + + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); + listener.expectedCode = 401; // OK + await openAndListen(chan); + + Assert.equal( + chan.getResponseHeader("cross-origin-embedder-policy"), + "require-corp" + ); + Assert.equal( + chan.getResponseHeader("cross-origin-opener-policy"), + "same-origin" + ); +}); + +// XXX(valentin): this makes tests fail if it's not run last. Why? +add_task(async function test_nonascii_xhr() { + await new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.open("GET", URL + "/auth/non_ascii", true, "é", "é"); + xhr.onreadystatechange = function (event) { + if (xhr.readyState == 4) { + Assert.equal(xhr.status, 200); + resolve(); + xhr.onreadystatechange = null; + } + }; + xhr.send(null); + }); +}); + +// PATH HANDLERS + +// /auth +function authHandler(metadata, response) { + // btoa("guest:guest"), but that function is not available here + var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; + + var body; + if ( + metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader + ) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "success"; + } else { + // didn't know guest:guest, failure + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "failed"; + } + + response.bodyOutputStream.write(body, body.length); +} + +// /auth/ntlm/simple +function authNtlmSimple(metadata, response) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader( + "WWW-Authenticate", + "NTLM" /* + ' realm="secret"' */, + false + ); + + var body = + "NOTE: This just sends an NTLM challenge, it never\n" + + "accepts the authentication. It also closes\n" + + "the connection after sending the challenge\n"; + + response.bodyOutputStream.write(body, body.length); +} + +// /auth/realm +function authRealm(metadata, response) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="\\"f\\oo_bar"', false); + var body = "success"; + + response.bodyOutputStream.write(body, body.length); +} + +// /auth/nonAscii +function authNonascii(metadata, response) { + // btoa("é:é"), but that function is not available here + var expectedHeader = "Basic w6k6w6k="; + + var body; + if ( + metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader + ) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + // Use correct XML syntax since this function is also used for testing XHR. + body = "<?xml version='1.0' ?><root>success</root>"; + } else { + // didn't know é:é, failure + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "<?xml version='1.0' ?><root>failed</root>"; + } + + response.bodyOutputStream.write(body, body.length); +} + +function corpAndCoep(metadata, response) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("cross-origin-embedder-policy", "require-corp"); + response.setHeader("cross-origin-opener-policy", "same-origin"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); +} + +// +// Digest functions +// +function bytesFromString(str) { + var converter = Cc[ + "@mozilla.org/intl/scriptableunicodeconverter" + ].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + var data = converter.convertToByteArray(str); + return data; +} + +// return the two-digit hexadecimal code for a byte +function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); +} + +function HMD5(str) { + var data = bytesFromString(str); + var ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + ch.init(Ci.nsICryptoHash.MD5); + ch.update(data, data.length); + var hash = ch.finish(false); + return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join(""); +} + +function HSHA256(str) { + var data = bytesFromString(str); + var ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + ch.init(Ci.nsICryptoHash.SHA256); + ch.update(data, data.length); + var hash = ch.finish(false); + return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join(""); +} + +// +// Digest handler +// +// /auth/digest +function authDigestMD5_helper(metadata, response, test_name) { + var nonce = "6f93719059cf8d568005727f3250e798"; + var opaque = "1234opaque1234"; + var body; + var send_401 = 0; + // check creds if we have them + if (metadata.hasHeader("Authorization")) { + var cnonceRE = /cnonce="(\w+)"/; + var responseRE = /response="(\w+)"/; + var usernameRE = /username="(\w+)"/; + var algorithmRE = /algorithm=([\w-]+)/; + var auth = metadata.getHeader("Authorization"); + var cnonce = auth.match(cnonceRE)[1]; + var clientDigest = auth.match(responseRE)[1]; + var username = auth.match(usernameRE)[1]; + var algorithm = auth.match(algorithmRE)[1]; + var nc = "00000001"; + + if (username != "guest") { + response.setStatusLine(metadata.httpVersion, 400, "bad request"); + body = "should never get here"; + } else if ( + algorithm != null && + algorithm != "MD5" && + algorithm != "MD5-sess" + ) { + response.setStatusLine(metadata.httpVersion, 400, "bad request"); + body = "Algorithm must be same as provided in WWW-Authenticate header"; + } else { + // see RFC2617 for the description of this calculation + var A1 = "guest:secret:guest"; + if (algorithm == "MD5-sess") { + A1 = [HMD5(A1), nonce, cnonce].join(":"); + } + var A2 = "GET:/auth/" + test_name; + var noncebits = [nonce, nc, cnonce, "auth", HMD5(A2)].join(":"); + var digest = HMD5([HMD5(A1), noncebits].join(":")); + + if (clientDigest == digest) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + body = "success"; + } else { + send_401 = 1; + body = "auth failed"; + } + } + } else { + // no header, send one + send_401 = 1; + body = "failed, no header"; + } + + if (send_401) { + var authenticate_md5 = + 'Digest realm="secret", domain="/", qop=auth,' + + 'algorithm=MD5, nonce="' + + nonce + + '" opaque="' + + opaque + + '"'; + var authenticate_md5sess = + 'Digest realm="secret", domain="/", qop=auth,' + + 'algorithm=MD5, nonce="' + + nonce + + '" opaque="' + + opaque + + '"'; + if (test_name == "digest_md5") { + response.setHeader("WWW-Authenticate", authenticate_md5, false); + } else if (test_name == "digest_md5sess") { + response.setHeader("WWW-Authenticate", authenticate_md5sess, false); + } + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + } + + response.bodyOutputStream.write(body, body.length); +} + +function authDigestMD5(metadata, response) { + authDigestMD5_helper(metadata, response, "digest_md5"); +} + +function authDigestMD5sess(metadata, response) { + authDigestMD5_helper(metadata, response, "digest_md5sess"); +} + +function authDigestSHA256_helper(metadata, response, test_name) { + var nonce = "6f93719059cf8d568005727f3250e798"; + var opaque = "1234opaque1234"; + var body; + var send_401 = 0; + // check creds if we have them + if (metadata.hasHeader("Authorization")) { + var cnonceRE = /cnonce="(\w+)"/; + var responseRE = /response="(\w+)"/; + var usernameRE = /username="(\w+)"/; + var algorithmRE = /algorithm=([\w-]+)/; + var auth = metadata.getHeader("Authorization"); + var cnonce = auth.match(cnonceRE)[1]; + var clientDigest = auth.match(responseRE)[1]; + var username = auth.match(usernameRE)[1]; + var algorithm = auth.match(algorithmRE)[1]; + var nc = "00000001"; + + if (username != "guest") { + response.setStatusLine(metadata.httpVersion, 400, "bad request"); + body = "should never get here"; + } else if (algorithm != "SHA-256" && algorithm != "SHA-256-sess") { + response.setStatusLine(metadata.httpVersion, 400, "bad request"); + body = "Algorithm must be same as provided in WWW-Authenticate header"; + } else { + // see RFC7616 for the description of this calculation + var A1 = "guest:secret:guest"; + if (algorithm == "SHA-256-sess") { + A1 = [HSHA256(A1), nonce, cnonce].join(":"); + } + var A2 = "GET:/auth/" + test_name; + var noncebits = [nonce, nc, cnonce, "auth", HSHA256(A2)].join(":"); + var digest = HSHA256([HSHA256(A1), noncebits].join(":")); + + if (clientDigest == digest) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + body = "success"; + } else { + send_401 = 1; + body = "auth failed"; + } + } + } else { + // no header, send one + send_401 = 1; + body = "failed, no header"; + } + + if (send_401) { + var authenticate_sha256 = + 'Digest realm="secret", domain="/", qop=auth, ' + + 'algorithm=SHA-256, nonce="' + + nonce + + '", opaque="' + + opaque + + '"'; + var authenticate_sha256sess = + 'Digest realm="secret", domain="/", qop=auth, ' + + 'algorithm=SHA-256-sess, nonce="' + + nonce + + '", opaque="' + + opaque + + '"'; + var authenticate_md5 = + 'Digest realm="secret", domain="/", qop=auth, ' + + 'algorithm=MD5, nonce="' + + nonce + + '", opaque="' + + opaque + + '"'; + if (test_name == "digest_sha256") { + response.setHeader("WWW-Authenticate", authenticate_sha256, false); + } else if (test_name == "digest_sha256sess") { + response.setHeader("WWW-Authenticate", authenticate_sha256sess, false); + } else if (test_name == "digest_md5_sha256") { + response.setHeader("WWW-Authenticate", authenticate_md5, false); + response.setHeader("WWW-Authenticate", authenticate_sha256, true); + } else if (test_name == "digest_md5_sha256_oneline") { + response.setHeader( + "WWW-Authenticate", + authenticate_md5 + " " + authenticate_sha256, + false + ); + } else if (test_name == "digest_sha256_md5") { + response.setHeader("WWW-Authenticate", authenticate_sha256, false); + response.setHeader("WWW-Authenticate", authenticate_md5, true); + } + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + } + + response.bodyOutputStream.write(body, body.length); +} + +function authDigestSHA256(metadata, response) { + authDigestSHA256_helper(metadata, response, "digest_sha256"); +} + +function authDigestSHA256sess(metadata, response) { + authDigestSHA256_helper(metadata, response, "digest_sha256sess"); +} + +function authDigestSHA256_MD5(metadata, response) { + authDigestSHA256_helper(metadata, response, "digest_sha256_md5"); +} + +function authDigestMD5_SHA256(metadata, response) { + authDigestSHA256_helper(metadata, response, "digest_md5_sha256"); +} + +function authDigestMD5_SHA256_oneline(metadata, response) { + authDigestSHA256_helper(metadata, response, "digest_md5_sha256_oneline"); +} + +function authShortDigest(metadata, response) { + // no header, send one + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", "Digest", false); +} + +let buildLargePayload = (function () { + let size = 33 * 1024; + let ret = ""; + return function () { + // Return cached value. + if (ret.length) { + return ret; + } + for (let i = 0; i < size; i++) { + ret += "a"; + } + return ret; + }; +})(); + +function largeRealm(metadata, response) { + // test > 32KB realm tokens + var body; + + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader( + "WWW-Authenticate", + 'Digest realm="' + buildLargePayload() + '", domain="foo"' + ); + + body = "need to authenticate"; + response.bodyOutputStream.write(body, body.length); +} + +function largeDomain(metadata, response) { + // test > 32KB domain tokens + var body; + + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader( + "WWW-Authenticate", + 'Digest realm="foo", domain="' + buildLargePayload() + '"' + ); + + body = "need to authenticate"; + response.bodyOutputStream.write(body, body.length); +} + +add_task(async function test_large_realm() { + var chan = makeChan(URL + "/largeRealm", URL); + + listener.expectedCode = 401; // Unauthorized + await openAndListen(chan); +}); + +add_task(async function test_large_domain() { + var chan = makeChan(URL + "/largeDomain", URL); + + listener.expectedCode = 401; // Unauthorized + await openAndListen(chan); +}); + +async function add_parse_realm_testcase(testcase) { + httpserv.registerPathHandler("/parse_realm", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", testcase.input, false); + + let body = "failed"; + response.bodyOutputStream.write(body, body.length); + }); + + let chan = makeChan(URL + "/parse_realm", URL); + let requestor = new RealmTestRequestor(); + chan.notificationCallbacks = requestor; + + listener.expectedCode = 401; + await openAndListen(chan); + Assert.equal(requestor.promptRealm, testcase.realm); +} + +add_task(async function simplebasic() { + await add_parse_realm_testcase({ + input: `Basic realm="foo"`, + scheme: `Basic`, + realm: `foo`, + }); +}); + +add_task(async function simplebasiclf() { + await add_parse_realm_testcase({ + input: `Basic\r\n realm="foo"`, + scheme: `Basic`, + realm: `foo`, + }); +}); + +add_task(async function simplebasicucase() { + await add_parse_realm_testcase({ + input: `BASIC REALM="foo"`, + scheme: `Basic`, + realm: `foo`, + }); +}); + +add_task(async function simplebasictok() { + await add_parse_realm_testcase({ + input: `Basic realm=foo`, + scheme: `Basic`, + realm: `foo`, + }); +}); + +add_task(async function simplebasictokbs() { + await add_parse_realm_testcase({ + input: `Basic realm=\\f\\o\\o`, + scheme: `Basic`, + realm: `\\foo`, + }); +}); + +add_task(async function simplebasicsq() { + await add_parse_realm_testcase({ + input: `Basic realm='foo'`, + scheme: `Basic`, + realm: `'foo'`, + }); +}); + +add_task(async function simplebasicpct() { + await add_parse_realm_testcase({ + input: `Basic realm="foo%20bar"`, + scheme: `Basic`, + realm: `foo%20bar`, + }); +}); + +add_task(async function simplebasiccomma() { + await add_parse_realm_testcase({ + input: `Basic , realm="foo"`, + scheme: `Basic`, + realm: `foo`, + }); +}); + +add_task(async function simplebasiccomma2() { + await add_parse_realm_testcase({ + input: `Basic, realm="foo"`, + scheme: `Basic`, + realm: ``, + }); +}); + +add_task(async function simplebasicnorealm() { + await add_parse_realm_testcase({ + input: `Basic`, + scheme: `Basic`, + realm: ``, + }); +}); + +add_task(async function simplebasic2realms() { + await add_parse_realm_testcase({ + input: `Basic realm="foo", realm="bar"`, + scheme: `Basic`, + realm: `foo`, + }); +}); + +add_task(async function simplebasicwsrealm() { + await add_parse_realm_testcase({ + input: `Basic realm = "foo"`, + scheme: `Basic`, + realm: `foo`, + }); +}); + +add_task(async function simplebasicrealmsqc() { + await add_parse_realm_testcase({ + input: `Basic realm="\\f\\o\\o"`, + scheme: `Basic`, + realm: `foo`, + }); +}); + +add_task(async function simplebasicrealmsqc2() { + await add_parse_realm_testcase({ + input: `Basic realm="\\"foo\\""`, + scheme: `Basic`, + realm: `"foo"`, + }); +}); + +add_task(async function simplebasicnewparam1() { + await add_parse_realm_testcase({ + input: `Basic realm="foo", bar="xyz",, a=b,,,c=d`, + scheme: `Basic`, + realm: `foo`, + }); +}); + +add_task(async function simplebasicnewparam2() { + await add_parse_realm_testcase({ + input: `Basic bar="xyz", realm="foo"`, + scheme: `Basic`, + realm: `foo`, + }); +}); + +add_task(async function simplebasicrealmiso88591() { + await add_parse_realm_testcase({ + input: `Basic realm="foo-ä"`, + scheme: `Basic`, + realm: `foo-ä`, + }); +}); + +add_task(async function simplebasicrealmutf8() { + await add_parse_realm_testcase({ + input: `Basic realm="foo-ä"`, + scheme: `Basic`, + realm: `foo-ä`, + }); +}); + +add_task(async function simplebasicrealmrfc2047() { + await add_parse_realm_testcase({ + input: `Basic realm="=?ISO-8859-1?Q?foo-=E4?="`, + scheme: `Basic`, + realm: `=?ISO-8859-1?Q?foo-=E4?=`, + }); +}); + +add_task(async function multibasicunknown() { + await add_parse_realm_testcase({ + input: `Basic realm="basic", Newauth realm="newauth"`, + scheme: `Basic`, + realm: `basic`, + }); +}); + +add_task(async function multibasicunknownnoparam() { + await add_parse_realm_testcase({ + input: `Basic realm="basic", Newauth`, + scheme: `Basic`, + realm: `basic`, + }); +}); + +add_task(async function multibasicunknown2() { + await add_parse_realm_testcase({ + input: `Newauth realm="newauth", Basic realm="basic"`, + scheme: `Basic`, + realm: `basic`, + }); +}); + +add_task(async function multibasicunknown2np() { + await add_parse_realm_testcase({ + input: `Newauth, Basic realm="basic"`, + scheme: `Basic`, + realm: `basic`, + }); +}); + +add_task(async function multibasicunknown2mf() { + httpserv.registerPathHandler("/parse_realm", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Newauth realm="newauth"`, false); + response.setHeader("WWW-Authenticate", `Basic realm="basic"`, false); + + let body = "failed"; + response.bodyOutputStream.write(body, body.length); + }); + + let chan = makeChan(URL + "/parse_realm", URL); + let requestor = new RealmTestRequestor(); + chan.notificationCallbacks = requestor; + + listener.expectedCode = 401; + await openAndListen(chan); + Assert.equal(requestor.promptRealm, "basic"); +}); + +add_task(async function multibasicempty() { + await add_parse_realm_testcase({ + input: `,Basic realm="basic"`, + scheme: `Basic`, + realm: `basic`, + }); +}); + +add_task(async function multibasicqs() { + await add_parse_realm_testcase({ + input: `Newauth realm="apps", type=1, title="Login to \"apps\"", Basic realm="simple"`, + scheme: `Basic`, + realm: `simple`, + }); +}); + +add_task(async function multidisgscheme() { + await add_parse_realm_testcase({ + input: `Newauth realm="Newauth Realm", basic=foo, Basic realm="Basic Realm"`, + scheme: `Basic`, + realm: `Basic Realm`, + }); +}); + +add_task(async function unknown() { + await add_parse_realm_testcase({ + input: `Newauth param="value"`, + scheme: `Basic`, + realm: ``, + }); +}); + +add_task(async function parametersnotrequired() { + await add_parse_realm_testcase({ input: `A, B`, scheme: `Basic`, realm: `` }); +}); + +add_task(async function disguisedrealm() { + await add_parse_realm_testcase({ + input: `Basic foo="realm=nottherealm", realm="basic"`, + scheme: `Basic`, + realm: `basic`, + }); +}); + +add_task(async function disguisedrealm2() { + await add_parse_realm_testcase({ + input: `Basic nottherealm="nottherealm", realm="basic"`, + scheme: `Basic`, + realm: `basic`, + }); +}); + +add_task(async function missingquote() { + await add_parse_realm_testcase({ + input: `Basic realm="basic`, + scheme: `Basic`, + realm: `basic`, + }); +}); diff --git a/netwerk/test/unit/test_authpromptwrapper.js b/netwerk/test/unit/test_authpromptwrapper.js new file mode 100644 index 0000000000..69680354ab --- /dev/null +++ b/netwerk/test/unit/test_authpromptwrapper.js @@ -0,0 +1,207 @@ +// NOTE: This tests code outside of Necko. The test still lives here because +// the contract is part of Necko. + +// TODO: +// - HTTPS +// - Proxies + +"use strict"; + +const nsIAuthInformation = Ci.nsIAuthInformation; +const nsIAuthPromptAdapterFactory = Ci.nsIAuthPromptAdapterFactory; + +function run_test() { + const contractID = "@mozilla.org/network/authprompt-adapter-factory;1"; + if (!(contractID in Cc)) { + print("No adapter factory found, skipping testing"); + return; + } + var adapter = Cc[contractID].getService(); + Assert.equal(adapter instanceof nsIAuthPromptAdapterFactory, true); + + // NOTE: xpconnect lets us get away with passing an empty object here + // For this part of the test, we only care that this function returns + // success + Assert.notEqual(adapter.createAdapter({}), null); + + const host = "www.mozilla.org"; + + var info = { + username: "", + password: "", + domain: "", + + flags: nsIAuthInformation.AUTH_HOST, + authenticationScheme: "basic", + realm: "secretrealm", + }; + + const CALLED_PROMPT = 1 << 0; + const CALLED_PROMPTUP = 1 << 1; + const CALLED_PROMPTP = 1 << 2; + function Prompt1() {} + Prompt1.prototype = { + called: 0, + rv: true, + + user: "foo\\bar", + pw: "bar", + + scheme: "http", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]), + + prompt: function ap1_prompt(title, text, realm, save, defaultText, result) { + this.called |= CALLED_PROMPT; + this.doChecks(text, realm); + return this.rv; + }, + + promptUsernameAndPassword: function ap1_promptUP( + title, + text, + realm, + savePW, + user, + pw + ) { + this.called |= CALLED_PROMPTUP; + this.doChecks(text, realm); + user.value = this.user; + pw.value = this.pw; + return this.rv; + }, + + promptPassword: function ap1_promptPW(title, text, realm, save, pwd) { + this.called |= CALLED_PROMPTP; + this.doChecks(text, realm); + pwd.value = this.pw; + return this.rv; + }, + + doChecks: function ap1_check(text, realm) { + Assert.equal(this.scheme + "://" + host + " (" + info.realm + ")", realm); + + Assert.notEqual(text.indexOf(host), -1); + if (info.flags & nsIAuthInformation.ONLY_PASSWORD) { + // Should have the username in the text + Assert.notEqual(text.indexOf(info.username), -1); + } else { + // Make sure that we show the realm if we have one and that we don't + // show "" otherwise + if (info.realm != "") { + Assert.notEqual(text.indexOf(info.realm), -1); + } else { + Assert.equal(text.indexOf('""'), -1); + } + // No explicit port in the URL; message should not contain -1 + // for those cases + Assert.equal(text.indexOf("-1"), -1); + } + }, + }; + + // Also have to make up a channel + var uri = NetUtil.newURI("http://" + host); + var chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + + function do_tests(expectedRV) { + var prompt1; + var wrapper; + + // 1: The simple case + prompt1 = new Prompt1(); + prompt1.rv = expectedRV; + wrapper = adapter.createAdapter(prompt1); + + var rv = wrapper.promptAuth(chan, 0, info); + Assert.equal(rv, prompt1.rv); + Assert.equal(prompt1.called, CALLED_PROMPTUP); + + if (rv) { + Assert.equal(info.domain, ""); + Assert.equal(info.username, prompt1.user); + Assert.equal(info.password, prompt1.pw); + } + + info.domain = ""; + info.username = ""; + info.password = ""; + + // 2: Only ask for a PW + prompt1 = new Prompt1(); + prompt1.rv = expectedRV; + info.flags |= nsIAuthInformation.ONLY_PASSWORD; + + // Initialize the username so that the prompt can show it + info.username = prompt1.user; + + wrapper = adapter.createAdapter(prompt1); + rv = wrapper.promptAuth(chan, 0, info); + Assert.equal(rv, prompt1.rv); + Assert.equal(prompt1.called, CALLED_PROMPTP); + + if (rv) { + Assert.equal(info.domain, ""); + Assert.equal(info.username, prompt1.user); // we initialized this + Assert.equal(info.password, prompt1.pw); + } + + info.flags &= ~nsIAuthInformation.ONLY_PASSWORD; + + info.domain = ""; + info.username = ""; + info.password = ""; + + // 3: user, pw and domain + prompt1 = new Prompt1(); + prompt1.rv = expectedRV; + info.flags |= nsIAuthInformation.NEED_DOMAIN; + + wrapper = adapter.createAdapter(prompt1); + rv = wrapper.promptAuth(chan, 0, info); + Assert.equal(rv, prompt1.rv); + Assert.equal(prompt1.called, CALLED_PROMPTUP); + + if (rv) { + Assert.equal(info.domain, "foo"); + Assert.equal(info.username, "bar"); + Assert.equal(info.password, prompt1.pw); + } + + info.flags &= ~nsIAuthInformation.NEED_DOMAIN; + + info.domain = ""; + info.username = ""; + info.password = ""; + + // 4: username that doesn't contain a domain + prompt1 = new Prompt1(); + prompt1.rv = expectedRV; + info.flags |= nsIAuthInformation.NEED_DOMAIN; + + prompt1.user = "foo"; + + wrapper = adapter.createAdapter(prompt1); + rv = wrapper.promptAuth(chan, 0, info); + Assert.equal(rv, prompt1.rv); + Assert.equal(prompt1.called, CALLED_PROMPTUP); + + if (rv) { + Assert.equal(info.domain, ""); + Assert.equal(info.username, prompt1.user); + Assert.equal(info.password, prompt1.pw); + } + + info.flags &= ~nsIAuthInformation.NEED_DOMAIN; + + info.domain = ""; + info.username = ""; + info.password = ""; + } + do_tests(true); + do_tests(false); +} diff --git a/netwerk/test/unit/test_backgroundfilesaver.js b/netwerk/test/unit/test_backgroundfilesaver.js new file mode 100644 index 0000000000..af99acb2b4 --- /dev/null +++ b/netwerk/test/unit/test_backgroundfilesaver.js @@ -0,0 +1,761 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests components that implement nsIBackgroundFileSaver. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs", +}); + +const BackgroundFileSaverOutputStream = Components.Constructor( + "@mozilla.org/network/background-file-saver;1?mode=outputstream", + "nsIBackgroundFileSaver" +); + +const BackgroundFileSaverStreamListener = Components.Constructor( + "@mozilla.org/network/background-file-saver;1?mode=streamlistener", + "nsIBackgroundFileSaver" +); + +const StringInputStream = Components.Constructor( + "@mozilla.org/io/string-input-stream;1", + "nsIStringInputStream", + "setData" +); + +const REQUEST_SUSPEND_AT = 1024 * 1024 * 4; +const TEST_DATA_SHORT = "This test string is written to the file."; +const TEST_FILE_NAME_1 = "test-backgroundfilesaver-1.txt"; +const TEST_FILE_NAME_2 = "test-backgroundfilesaver-2.txt"; +const TEST_FILE_NAME_3 = "test-backgroundfilesaver-3.txt"; + +// A map of test data length to the expected SHA-256 hashes +const EXPECTED_HASHES = { + // No data + 0: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + // TEST_DATA_SHORT + 40: "f37176b690e8744ee990a206c086cba54d1502aa2456c3b0c84ef6345d72a192", + // TEST_DATA_SHORT + TEST_DATA_SHORT + 80: "780c0e91f50bb7ec922cc11e16859e6d5df283c0d9470f61772e3d79f41eeb58", + // TEST_DATA_LONG + 4718592: "372cb9e5ce7b76d3e2a5042e78aa72dcf973e659a262c61b7ff51df74b36767b", + // TEST_DATA_LONG + TEST_DATA_LONG + 9437184: "693e4f8c6855a6fed4f5f9370d12cc53105672f3ff69783581e7d925984c41d3", +}; + +// Generate a long string of data in a moderately fast way. +const TEST_256_CHARS = new Array(257).join("-"); +const DESIRED_LENGTH = REQUEST_SUSPEND_AT * 1.125; +const TEST_DATA_LONG = new Array(1 + DESIRED_LENGTH / 256).join(TEST_256_CHARS); +Assert.equal(TEST_DATA_LONG.length, DESIRED_LENGTH); + +/** + * Returns a reference to a temporary file that is guaranteed not to exist and + * is cleaned up later. See FileTestUtils.getTempFile for details. + */ +function getTempFile(leafName) { + return FileTestUtils.getTempFile(leafName); +} + +/** + * Helper function for converting a binary blob to its hex equivalent. + * + * @param str + * String possibly containing non-printable chars. + * @return A hex-encoded string. + */ +function toHex(str) { + var hex = ""; + for (var i = 0; i < str.length; i++) { + hex += ("0" + str.charCodeAt(i).toString(16)).slice(-2); + } + return hex; +} + +/** + * Ensures that the given file contents are equal to the given string. + * + * @param aFile + * nsIFile whose contents should be verified. + * @param aExpectedContents + * String containing the octets that are expected in the file. + * + * @return {Promise} + * @resolves When the operation completes. + * @rejects Never. + */ +function promiseVerifyContents(aFile, aExpectedContents) { + return new Promise(resolve => { + NetUtil.asyncFetch( + { + uri: NetUtil.newURI(aFile), + loadUsingSystemPrincipal: true, + }, + function (aInputStream, aStatus) { + Assert.ok(Components.isSuccessCode(aStatus)); + let contents = NetUtil.readInputStreamToString( + aInputStream, + aInputStream.available() + ); + if (contents.length <= TEST_DATA_SHORT.length * 2) { + Assert.equal(contents, aExpectedContents); + } else { + // Do not print the entire content string to the test log. + Assert.equal(contents.length, aExpectedContents.length); + Assert.ok(contents == aExpectedContents); + } + resolve(); + } + ); + }); +} + +/** + * Waits for the given saver object to complete. + * + * @param aSaver + * The saver, with the output stream or a stream listener implementation. + * @param aOnTargetChangeFn + * Optional callback invoked with the target file name when it changes. + * + * @return {Promise} + * @resolves When onSaveComplete is called with a success code. + * @rejects With an exception, if onSaveComplete is called with a failure code. + */ +function promiseSaverComplete(aSaver, aOnTargetChangeFn) { + return new Promise((resolve, reject) => { + aSaver.observer = { + onTargetChange: function BFSO_onSaveComplete(aSaver, aTarget) { + if (aOnTargetChangeFn) { + aOnTargetChangeFn(aTarget); + } + }, + onSaveComplete: function BFSO_onSaveComplete(aSaver, aStatus) { + if (Components.isSuccessCode(aStatus)) { + resolve(); + } else { + reject(new Components.Exception("Saver failed.", aStatus)); + } + }, + }; + }); +} + +/** + * Feeds a string to a BackgroundFileSaverOutputStream. + * + * @param aSourceString + * The source data to copy. + * @param aSaverOutputStream + * The BackgroundFileSaverOutputStream to feed. + * @param aCloseWhenDone + * If true, the output stream will be closed when the copy finishes. + * + * @return {Promise} + * @resolves When the copy completes with a success code. + * @rejects With an exception, if the copy fails. + */ +function promiseCopyToSaver(aSourceString, aSaverOutputStream, aCloseWhenDone) { + return new Promise((resolve, reject) => { + let inputStream = new StringInputStream( + aSourceString, + aSourceString.length + ); + let copier = Cc[ + "@mozilla.org/network/async-stream-copier;1" + ].createInstance(Ci.nsIAsyncStreamCopier); + copier.init( + inputStream, + aSaverOutputStream, + null, + false, + true, + 0x8000, + true, + aCloseWhenDone + ); + copier.asyncCopy( + { + onStartRequest() {}, + onStopRequest(aRequest, aStatusCode) { + if (Components.isSuccessCode(aStatusCode)) { + resolve(); + } else { + reject(new Components.Exception(aStatusCode)); + } + }, + }, + null + ); + }); +} + +/** + * Feeds a string to a BackgroundFileSaverStreamListener. + * + * @param aSourceString + * The source data to copy. + * @param aSaverStreamListener + * The BackgroundFileSaverStreamListener to feed. + * @param aCloseWhenDone + * If true, the output stream will be closed when the copy finishes. + * + * @return {Promise} + * @resolves When the operation completes with a success code. + * @rejects With an exception, if the operation fails. + */ +function promisePumpToSaver( + aSourceString, + aSaverStreamListener, + aCloseWhenDone +) { + return new Promise((resolve, reject) => { + aSaverStreamListener.QueryInterface(Ci.nsIStreamListener); + let inputStream = new StringInputStream( + aSourceString, + aSourceString.length + ); + let pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance( + Ci.nsIInputStreamPump + ); + pump.init(inputStream, 0, 0, true); + pump.asyncRead({ + onStartRequest: function PPTS_onStartRequest(aRequest) { + aSaverStreamListener.onStartRequest(aRequest); + }, + onStopRequest: function PPTS_onStopRequest(aRequest, aStatusCode) { + aSaverStreamListener.onStopRequest(aRequest, aStatusCode); + if (Components.isSuccessCode(aStatusCode)) { + resolve(); + } else { + reject(new Components.Exception(aStatusCode)); + } + }, + onDataAvailable: function PPTS_onDataAvailable( + aRequest, + aInputStream, + aOffset, + aCount + ) { + aSaverStreamListener.onDataAvailable( + aRequest, + aInputStream, + aOffset, + aCount + ); + }, + }); + }); +} + +var gStillRunning = true; + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +add_task(function test_setup() { + // Wait 10 minutes, that is half of the external xpcshell timeout. + do_timeout(10 * 60 * 1000, function () { + if (gStillRunning) { + do_throw("Test timed out."); + } + }); +}); + +add_task(async function test_normal() { + // This test demonstrates the most basic use case. + let destFile = getTempFile(TEST_FILE_NAME_1); + + // Create the object implementing the output stream. + let saver = new BackgroundFileSaverOutputStream(); + + // Set up callbacks for completion and target file name change. + let receivedOnTargetChange = false; + function onTargetChange(aTarget) { + Assert.ok(destFile.equals(aTarget)); + receivedOnTargetChange = true; + } + let completionPromise = promiseSaverComplete(saver, onTargetChange); + + // Set the target file. + saver.setTarget(destFile, false); + + // Write some data and close the output stream. + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + // Indicate that we are ready to finish, and wait for a successful callback. + saver.finish(Cr.NS_OK); + await completionPromise; + + // Only after we receive the completion notification, we can also be sure that + // we've received the target file name change notification before it. + Assert.ok(receivedOnTargetChange); + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_combinations() { + let initialFile = getTempFile(TEST_FILE_NAME_1); + let renamedFile = getTempFile(TEST_FILE_NAME_2); + + // Keep track of the current file. + let currentFile = null; + function onTargetChange(aTarget) { + currentFile = null; + info("Target file changed to: " + aTarget.leafName); + currentFile = aTarget; + } + + // Tests various combinations of events and behaviors for both the stream + // listener and the output stream implementations. + for (let testFlags = 0; testFlags < 32; testFlags++) { + let keepPartialOnFailure = !!(testFlags & 1); + let renameAtSomePoint = !!(testFlags & 2); + let cancelAtSomePoint = !!(testFlags & 4); + let useStreamListener = !!(testFlags & 8); + let useLongData = !!(testFlags & 16); + + let startTime = Date.now(); + info( + "Starting keepPartialOnFailure = " + + keepPartialOnFailure + + ", renameAtSomePoint = " + + renameAtSomePoint + + ", cancelAtSomePoint = " + + cancelAtSomePoint + + ", useStreamListener = " + + useStreamListener + + ", useLongData = " + + useLongData + ); + + // Create the object and register the observers. + currentFile = null; + let saver = useStreamListener + ? new BackgroundFileSaverStreamListener() + : new BackgroundFileSaverOutputStream(); + saver.enableSha256(); + let completionPromise = promiseSaverComplete(saver, onTargetChange); + + // Start feeding the first chunk of data to the saver. In case we are using + // the stream listener, we only write one chunk. + let testData = useLongData ? TEST_DATA_LONG : TEST_DATA_SHORT; + let feedPromise = useStreamListener + ? promisePumpToSaver(testData + testData, saver) + : promiseCopyToSaver(testData, saver, false); + + // Set a target output file. + saver.setTarget(initialFile, keepPartialOnFailure); + + // Wait for the first chunk of data to be copied. + await feedPromise; + + if (renameAtSomePoint) { + saver.setTarget(renamedFile, keepPartialOnFailure); + } + + if (cancelAtSomePoint) { + saver.finish(Cr.NS_ERROR_FAILURE); + } + + // Feed the second chunk of data to the saver. + if (!useStreamListener) { + await promiseCopyToSaver(testData, saver, true); + } + + // Wait for completion, and ensure we succeeded or failed as expected. + if (!cancelAtSomePoint) { + saver.finish(Cr.NS_OK); + } + try { + await completionPromise; + if (cancelAtSomePoint) { + do_throw("Failure expected."); + } + } catch (ex) { + if (!cancelAtSomePoint || ex.result != Cr.NS_ERROR_FAILURE) { + throw ex; + } + } + + if (!cancelAtSomePoint) { + // In this case, the file must exist. + Assert.ok(currentFile.exists()); + let expectedContents = testData + testData; + await promiseVerifyContents(currentFile, expectedContents); + Assert.equal( + EXPECTED_HASHES[expectedContents.length], + toHex(saver.sha256Hash) + ); + currentFile.remove(false); + + // If the target was really renamed, the old file should not exist. + if (renamedFile.equals(currentFile)) { + Assert.ok(!initialFile.exists()); + } + } else if (!keepPartialOnFailure) { + // In this case, the file must not exist. + Assert.ok(!initialFile.exists()); + Assert.ok(!renamedFile.exists()); + } else { + // In this case, the file may or may not exist, because canceling can + // interrupt the asynchronous operation at any point, even before the file + // has been created for the first time. + if (initialFile.exists()) { + initialFile.remove(false); + } + if (renamedFile.exists()) { + renamedFile.remove(false); + } + } + + info("Test case completed in " + (Date.now() - startTime) + " ms."); + } +}); + +add_task(async function test_setTarget_after_close_stream() { + // This test checks the case where we close the output stream before we call + // the setTarget method. All the data should be buffered and written anyway. + let destFile = getTempFile(TEST_FILE_NAME_1); + + // Test the case where the file does not already exists first, then the case + // where the file already exists. + for (let i = 0; i < 2; i++) { + let saver = new BackgroundFileSaverOutputStream(); + saver.enableSha256(); + let completionPromise = promiseSaverComplete(saver); + + // Copy some data to the output stream of the file saver. This data must + // be shorter than the internal component's pipe buffer for the test to + // succeed, because otherwise the test would block waiting for the write to + // complete. + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + // Set the target file and wait for the output to finish. + saver.setTarget(destFile, false); + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results. + await promiseVerifyContents(destFile, TEST_DATA_SHORT); + Assert.equal( + EXPECTED_HASHES[TEST_DATA_SHORT.length], + toHex(saver.sha256Hash) + ); + } + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_setTarget_fast() { + // This test checks a fast rename of the target file. + let destFile1 = getTempFile(TEST_FILE_NAME_1); + let destFile2 = getTempFile(TEST_FILE_NAME_2); + let saver = new BackgroundFileSaverOutputStream(); + let completionPromise = promiseSaverComplete(saver); + + // Set the initial name after the stream is closed, then rename immediately. + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + saver.setTarget(destFile1, false); + saver.setTarget(destFile2, false); + + // Wait for all the operations to complete. + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results and clean up. + Assert.ok(!destFile1.exists()); + await promiseVerifyContents(destFile2, TEST_DATA_SHORT); + destFile2.remove(false); +}); + +add_task(async function test_setTarget_multiple() { + // This test checks multiple renames of the target file. + let destFile = getTempFile(TEST_FILE_NAME_1); + let saver = new BackgroundFileSaverOutputStream(); + let completionPromise = promiseSaverComplete(saver); + + // Rename both before and after the stream is closed. + saver.setTarget(getTempFile(TEST_FILE_NAME_2), false); + saver.setTarget(getTempFile(TEST_FILE_NAME_3), false); + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + saver.setTarget(getTempFile(TEST_FILE_NAME_2), false); + saver.setTarget(destFile, false); + + // Wait for all the operations to complete. + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results and clean up. + Assert.ok(!getTempFile(TEST_FILE_NAME_2).exists()); + Assert.ok(!getTempFile(TEST_FILE_NAME_3).exists()); + await promiseVerifyContents(destFile, TEST_DATA_SHORT); + destFile.remove(false); +}); + +add_task(async function test_enableAppend() { + // This test checks append mode with hashing disabled. + let destFile = getTempFile(TEST_FILE_NAME_1); + + // Test the case where the file does not already exists first, then the case + // where the file already exists. + for (let i = 0; i < 2; i++) { + let saver = new BackgroundFileSaverOutputStream(); + saver.enableAppend(); + let completionPromise = promiseSaverComplete(saver); + + saver.setTarget(destFile, false); + await promiseCopyToSaver(TEST_DATA_LONG, saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results. + let expectedContents = + i == 0 ? TEST_DATA_LONG : TEST_DATA_LONG + TEST_DATA_LONG; + await promiseVerifyContents(destFile, expectedContents); + } + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_enableAppend_setTarget_fast() { + // This test checks a fast rename of the target file in append mode. + let destFile1 = getTempFile(TEST_FILE_NAME_1); + let destFile2 = getTempFile(TEST_FILE_NAME_2); + + // Test the case where the file does not already exists first, then the case + // where the file already exists. + for (let i = 0; i < 2; i++) { + let saver = new BackgroundFileSaverOutputStream(); + saver.enableAppend(); + let completionPromise = promiseSaverComplete(saver); + + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + // The first time, we start appending to the first file and rename to the + // second file. The second time, we start appending to the second file, + // that was created the first time, and rename back to the first file. + let firstFile = i == 0 ? destFile1 : destFile2; + let secondFile = i == 0 ? destFile2 : destFile1; + saver.setTarget(firstFile, false); + saver.setTarget(secondFile, false); + + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results. + Assert.ok(!firstFile.exists()); + let expectedContents = + i == 0 ? TEST_DATA_SHORT : TEST_DATA_SHORT + TEST_DATA_SHORT; + await promiseVerifyContents(secondFile, expectedContents); + } + + // Clean up. + destFile1.remove(false); +}); + +add_task(async function test_enableAppend_hash() { + // This test checks append mode, also verifying that the computed hash + // includes the contents of the existing data. + let destFile = getTempFile(TEST_FILE_NAME_1); + + // Test the case where the file does not already exists first, then the case + // where the file already exists. + for (let i = 0; i < 2; i++) { + let saver = new BackgroundFileSaverOutputStream(); + saver.enableAppend(); + saver.enableSha256(); + let completionPromise = promiseSaverComplete(saver); + + saver.setTarget(destFile, false); + await promiseCopyToSaver(TEST_DATA_LONG, saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results. + let expectedContents = + i == 0 ? TEST_DATA_LONG : TEST_DATA_LONG + TEST_DATA_LONG; + await promiseVerifyContents(destFile, expectedContents); + Assert.equal( + EXPECTED_HASHES[expectedContents.length], + toHex(saver.sha256Hash) + ); + } + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_finish_only() { + // This test checks creating the object and doing nothing. + let saver = new BackgroundFileSaverOutputStream(); + function onTargetChange(aTarget) { + do_throw("Should not receive the onTargetChange notification."); + } + let completionPromise = promiseSaverComplete(saver, onTargetChange); + saver.finish(Cr.NS_OK); + await completionPromise; +}); + +add_task(async function test_empty() { + // This test checks we still create an empty file when no data is fed. + let destFile = getTempFile(TEST_FILE_NAME_1); + + let saver = new BackgroundFileSaverOutputStream(); + let completionPromise = promiseSaverComplete(saver); + + saver.setTarget(destFile, false); + await promiseCopyToSaver("", saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results. + Assert.ok(destFile.exists()); + Assert.equal(destFile.fileSize, 0); + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_empty_hash() { + // This test checks the hash of an empty file, both in normal and append mode. + let destFile = getTempFile(TEST_FILE_NAME_1); + + // Test normal mode first, then append mode. + for (let i = 0; i < 2; i++) { + let saver = new BackgroundFileSaverOutputStream(); + if (i == 1) { + saver.enableAppend(); + } + saver.enableSha256(); + let completionPromise = promiseSaverComplete(saver); + + saver.setTarget(destFile, false); + await promiseCopyToSaver("", saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results. + Assert.equal(destFile.fileSize, 0); + Assert.equal(EXPECTED_HASHES[0], toHex(saver.sha256Hash)); + } + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_invalid_hash() { + let saver = new BackgroundFileSaverStreamListener(); + let completionPromise = promiseSaverComplete(saver); + // We shouldn't be able to get the hash if hashing hasn't been enabled + try { + saver.sha256Hash; + do_throw("Shouldn't be able to get hash if hashing not enabled"); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw ex; + } + } + // Enable hashing, but don't feed any data to saver + saver.enableSha256(); + let destFile = getTempFile(TEST_FILE_NAME_1); + saver.setTarget(destFile, false); + // We don't wait on promiseSaverComplete, so the hash getter can run before + // or after onSaveComplete is called. However, the expected behavior is the + // same in both cases since the hash is only valid when the save completes + // successfully. + saver.finish(Cr.NS_ERROR_FAILURE); + try { + saver.sha256Hash; + do_throw("Shouldn't be able to get hash if save did not succeed"); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw ex; + } + } + // Wait for completion so that the worker thread finishes dealing with the + // target file. We expect it to fail. + try { + await completionPromise; + do_throw("completionPromise should throw"); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_FAILURE) { + throw ex; + } + } +}); + +add_task(async function test_signature() { + // Check that we get a signature if the saver is finished. + let destFile = getTempFile(TEST_FILE_NAME_1); + + let saver = new BackgroundFileSaverOutputStream(); + let completionPromise = promiseSaverComplete(saver); + + try { + saver.signatureInfo; + do_throw("Can't get signature if saver is not complete"); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw ex; + } + } + + saver.enableSignatureInfo(); + saver.setTarget(destFile, false); + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + await promiseVerifyContents(destFile, TEST_DATA_SHORT); + + // signatureInfo is an empty nsIArray + Assert.equal(0, saver.signatureInfo.length); + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_signature_not_enabled() { + // Check that we get a signature if the saver is finished on Windows. + let destFile = getTempFile(TEST_FILE_NAME_1); + + let saver = new BackgroundFileSaverOutputStream(); + let completionPromise = promiseSaverComplete(saver); + saver.setTarget(destFile, false); + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + try { + saver.signatureInfo; + do_throw("Can't get signature if not enabled"); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw ex; + } + } + + // Clean up. + destFile.remove(false); +}); + +add_task(function test_teardown() { + gStillRunning = false; +}); diff --git a/netwerk/test/unit/test_be_conservative.js b/netwerk/test/unit/test_be_conservative.js new file mode 100644 index 0000000000..af8cf23976 --- /dev/null +++ b/netwerk/test/unit/test_be_conservative.js @@ -0,0 +1,256 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +"use strict"; + +// Allow telemetry probes which may otherwise be disabled for some +// applications (e.g. Thunderbird). +Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true +); + +// Tests that nsIHttpChannelInternal.beConservative correctly limits the use of +// advanced TLS features that may cause compatibility issues. Does so by +// starting a TLS server that requires the advanced features and then ensuring +// that a client that is set to be conservative will fail when connecting. + +// Get a profile directory and ensure PSM initializes NSS. +do_get_profile(); +Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + +class InputStreamCallback { + constructor(output) { + this.output = output; + this.stopped = false; + } + + onInputStreamReady(stream) { + info("input stream ready"); + if (this.stopped) { + info("input stream callback stopped - bailing"); + return; + } + let available = 0; + try { + available = stream.available(); + } catch (e) { + // onInputStreamReady may fire when the stream has been closed. + equal( + e.result, + Cr.NS_BASE_STREAM_CLOSED, + "error should be NS_BASE_STREAM_CLOSED" + ); + } + if (available > 0) { + let request = NetUtil.readInputStreamToString(stream, available, { + charset: "utf8", + }); + ok( + request.startsWith("GET / HTTP/1.1\r\n"), + "Should get a simple GET / HTTP/1.1 request" + ); + let response = + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 2\r\n" + + "Content-Type: text/plain\r\n" + + "\r\nOK"; + let written = this.output.write(response, response.length); + equal( + written, + response.length, + "should have been able to write entire response" + ); + } + this.output.close(); + info("done with input stream ready"); + } + + stop() { + this.stopped = true; + this.output.close(); + } +} + +class TLSServerSecurityObserver { + constructor(input, output) { + this.input = input; + this.output = output; + this.callbacks = []; + this.stopped = false; + } + + onHandshakeDone(socket, status) { + info("TLS handshake done"); + info(`TLS version used: ${status.tlsVersionUsed}`); + + if (this.stopped) { + info("handshake done callback stopped - bailing"); + return; + } + + let callback = new InputStreamCallback(this.output); + this.callbacks.push(callback); + this.input.asyncWait(callback, 0, 0, Services.tm.currentThread); + } + + stop() { + this.stopped = true; + this.input.close(); + this.output.close(); + this.callbacks.forEach(callback => { + callback.stop(); + }); + } +} + +class ServerSocketListener { + constructor() { + this.securityObservers = []; + } + + onSocketAccepted(socket, transport) { + info("accepted TLS client connection"); + let connectionInfo = transport.securityCallbacks.getInterface( + Ci.nsITLSServerConnectionInfo + ); + let input = transport.openInputStream(0, 0, 0); + let output = transport.openOutputStream(0, 0, 0); + let securityObserver = new TLSServerSecurityObserver(input, output); + this.securityObservers.push(securityObserver); + connectionInfo.setSecurityObserver(securityObserver); + } + + // For some reason we get input stream callback events after we've stopped + // listening, so this ensures we just drop those events. + onStopListening() { + info("onStopListening"); + this.securityObservers.forEach(observer => { + observer.stop(); + }); + } +} + +function startServer(cert, minServerVersion, maxServerVersion) { + let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance( + Ci.nsITLSServerSocket + ); + tlsServer.init(-1, true, -1); + tlsServer.serverCert = cert; + tlsServer.setVersionRange(minServerVersion, maxServerVersion); + tlsServer.setSessionTickets(false); + tlsServer.asyncListen(new ServerSocketListener()); + return tlsServer; +} + +const hostname = "example.com"; + +function storeCertOverride(port, cert) { + let certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" + ].getService(Ci.nsICertOverrideService); + certOverrideService.rememberValidityOverride(hostname, port, {}, cert, true); +} + +function startClient(port, beConservative, expectSuccess) { + HandshakeTelemetryHelpers.resetHistograms(); + let flavors = ["", "_FIRST_TRY"]; + let nonflavors = []; + if (beConservative) { + flavors.push("_CONSERVATIVE"); + nonflavors.push("_ECH"); + nonflavors.push("_ECH_GREASE"); + } else { + nonflavors.push("_CONSERVATIVE"); + } + + let req = new XMLHttpRequest(); + req.open("GET", `https://${hostname}:${port}`); + let internalChannel = req.channel.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.beConservative = beConservative; + return new Promise((resolve, reject) => { + req.onload = () => { + ok( + expectSuccess, + `should ${expectSuccess ? "" : "not "}have gotten load event` + ); + equal(req.responseText, "OK", "response text should be 'OK'"); + + // Only check telemetry if network process is disabled. + if (!mozinfo.socketprocess_networking) { + HandshakeTelemetryHelpers.checkSuccess(flavors); + HandshakeTelemetryHelpers.checkEmpty(nonflavors); + } + + resolve(); + }; + req.onerror = () => { + ok( + !expectSuccess, + `should ${!expectSuccess ? "" : "not "}have gotten an error` + ); + + // Only check telemetry if network process is disabled. + if (!mozinfo.socketprocess_networking) { + // 98 is SSL_ERROR_PROTOCOL_VERSION_ALERT (see sslerr.h) + HandshakeTelemetryHelpers.checkEntry(flavors, 98, 1); + HandshakeTelemetryHelpers.checkEmpty(nonflavors); + } + + resolve(); + }; + + req.send(); + }); +} + +add_task(async function () { + Services.prefs.setIntPref("security.tls.version.max", 4); + Services.prefs.setCharPref("network.dns.localDomains", hostname); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + let cert = getTestServerCertificate(); + + // First run a server that accepts TLS 1.2 and 1.3. A conservative client + // should succeed in connecting. + let server = startServer( + cert, + Ci.nsITLSClientStatus.TLS_VERSION_1_2, + Ci.nsITLSClientStatus.TLS_VERSION_1_3 + ); + storeCertOverride(server.port, cert); + await startClient( + server.port, + true /*be conservative*/, + true /*should succeed*/ + ); + server.close(); + + // Now run a server that only accepts TLS 1.3. A conservative client will not + // succeed in this case. + server = startServer( + cert, + Ci.nsITLSClientStatus.TLS_VERSION_1_3, + Ci.nsITLSClientStatus.TLS_VERSION_1_3 + ); + storeCertOverride(server.port, cert); + await startClient( + server.port, + true /*be conservative*/, + false /*should fail*/ + ); + + // However, a non-conservative client should succeed. + await startClient( + server.port, + false /*don't be conservative*/, + true /*should succeed*/ + ); + server.close(); +}); + +registerCleanupFunction(function () { + Services.prefs.clearUserPref("security.tls.version.max"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); +}); diff --git a/netwerk/test/unit/test_be_conservative_error_handling.js b/netwerk/test/unit/test_be_conservative_error_handling.js new file mode 100644 index 0000000000..eae3042592 --- /dev/null +++ b/netwerk/test/unit/test_be_conservative_error_handling.js @@ -0,0 +1,216 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +"use strict"; + +// Tests that nsIHttpChannelInternal.beConservative correctly limits the use of +// advanced TLS features that may cause compatibility issues. Does so by +// starting a TLS server that requires the advanced features and then ensuring +// that a client that is set to be conservative will fail when connecting. + +// Get a profile directory and ensure PSM initializes NSS. +do_get_profile(); +Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + +class InputStreamCallback { + constructor(output) { + this.output = output; + this.stopped = false; + } + + onInputStreamReady(stream) { + info("input stream ready"); + if (this.stopped) { + info("input stream callback stopped - bailing"); + return; + } + let available = 0; + try { + available = stream.available(); + } catch (e) { + // onInputStreamReady may fire when the stream has been closed. + equal( + e.result, + Cr.NS_BASE_STREAM_CLOSED, + "error should be NS_BASE_STREAM_CLOSED" + ); + } + if (available > 0) { + let request = NetUtil.readInputStreamToString(stream, available, { + charset: "utf8", + }); + ok( + request.startsWith("GET / HTTP/1.1\r\n"), + "Should get a simple GET / HTTP/1.1 request" + ); + let response = + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 2\r\n" + + "Content-Type: text/plain\r\n" + + "\r\nOK"; + let written = this.output.write(response, response.length); + equal( + written, + response.length, + "should have been able to write entire response" + ); + } + this.output.close(); + info("done with input stream ready"); + } + + stop() { + this.stopped = true; + this.output.close(); + } +} + +class TLSServerSecurityObserver { + constructor(input, output) { + this.input = input; + this.output = output; + this.callbacks = []; + this.stopped = false; + } + + onHandshakeDone(socket, status) { + info("TLS handshake done"); + info(`TLS version used: ${status.tlsVersionUsed}`); + + if (this.stopped) { + info("handshake done callback stopped - bailing"); + return; + } + + let callback = new InputStreamCallback(this.output); + this.callbacks.push(callback); + this.input.asyncWait(callback, 0, 0, Services.tm.currentThread); + } + + stop() { + this.stopped = true; + this.input.close(); + this.output.close(); + this.callbacks.forEach(callback => { + callback.stop(); + }); + } +} + +class ServerSocketListener { + constructor() { + this.securityObservers = []; + } + + onSocketAccepted(socket, transport) { + info("accepted TLS client connection"); + let connectionInfo = transport.securityCallbacks.getInterface( + Ci.nsITLSServerConnectionInfo + ); + let input = transport.openInputStream(0, 0, 0); + let output = transport.openOutputStream(0, 0, 0); + let securityObserver = new TLSServerSecurityObserver(input, output); + this.securityObservers.push(securityObserver); + connectionInfo.setSecurityObserver(securityObserver); + } + + // For some reason we get input stream callback events after we've stopped + // listening, so this ensures we just drop those events. + onStopListening() { + info("onStopListening"); + this.securityObservers.forEach(observer => { + observer.stop(); + }); + } +} + +function startServer(cert, minServerVersion, maxServerVersion) { + let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance( + Ci.nsITLSServerSocket + ); + tlsServer.init(-1, true, -1); + tlsServer.serverCert = cert; + tlsServer.setVersionRange(minServerVersion, maxServerVersion); + tlsServer.setSessionTickets(false); + tlsServer.asyncListen(new ServerSocketListener()); + return tlsServer; +} + +const hostname = "example.com"; + +function storeCertOverride(port, cert) { + let certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" + ].getService(Ci.nsICertOverrideService); + certOverrideService.rememberValidityOverride(hostname, port, {}, cert, true); +} + +function startClient(port, beConservative, expectSuccess) { + let req = new XMLHttpRequest(); + req.open("GET", `https://${hostname}:${port}`); + let internalChannel = req.channel.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.beConservative = beConservative; + return new Promise((resolve, reject) => { + req.onload = () => { + ok( + expectSuccess, + `should ${expectSuccess ? "" : "not "}have gotten load event` + ); + equal(req.responseText, "OK", "response text should be 'OK'"); + resolve(); + }; + req.onerror = () => { + ok( + !expectSuccess, + `should ${!expectSuccess ? "" : "not "}have gotten an error` + ); + resolve(); + }; + + req.send(); + }); +} + +add_task(async function () { + // Restrict to only TLS 1.3. + Services.prefs.setIntPref("security.tls.version.min", 4); + Services.prefs.setIntPref("security.tls.version.max", 4); + Services.prefs.setCharPref("network.dns.localDomains", hostname); + let cert = getTestServerCertificate(); + + // Run a server that accepts TLS 1.2 and 1.3. The connection should succeed. + let server = startServer( + cert, + Ci.nsITLSClientStatus.TLS_VERSION_1_2, + Ci.nsITLSClientStatus.TLS_VERSION_1_3 + ); + storeCertOverride(server.port, cert); + await startClient( + server.port, + true /*be conservative*/, + true /*should succeed*/ + ); + server.close(); + + // Now run a server that only accepts TLS 1.3. A conservative client will not + // succeed in this case. + server = startServer( + cert, + Ci.nsITLSClientStatus.TLS_VERSION_1_3, + Ci.nsITLSClientStatus.TLS_VERSION_1_3 + ); + storeCertOverride(server.port, cert); + await startClient( + server.port, + true /*be conservative*/, + false /*should fail*/ + ); + server.close(); +}); + +registerCleanupFunction(function () { + Services.prefs.clearUserPref("security.tls.version.min"); + Services.prefs.clearUserPref("security.tls.version.max"); + Services.prefs.clearUserPref("network.dns.localDomains"); +}); diff --git a/netwerk/test/unit/test_bhttp.js b/netwerk/test/unit/test_bhttp.js new file mode 100644 index 0000000000..61c8066a79 --- /dev/null +++ b/netwerk/test/unit/test_bhttp.js @@ -0,0 +1,220 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Unit tests for the binary http bindings. +// Tests basic encoding and decoding of requests and responses. + +function BinaryHttpRequest( + method, + scheme, + authority, + path, + headerNames, + headerValues, + content +) { + this.method = method; + this.scheme = scheme; + this.authority = authority; + this.path = path; + this.headerNames = headerNames; + this.headerValues = headerValues; + this.content = content; +} + +BinaryHttpRequest.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpRequest"]), +}; + +function test_encode_request() { + let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService( + Ci.nsIBinaryHttp + ); + let request = new BinaryHttpRequest( + "GET", + "https", + "", + "/hello.txt", + ["user-agent", "host", "accept-language"], + [ + "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3", + "www.example.com", + "en, mi", + ], + [] + ); + let encoded = bhttp.encodeRequest(request); + // This example is from RFC 9292. + let expected = hexStringToBytes( + "0003474554056874747073000a2f6865" + + "6c6c6f2e747874406c0a757365722d61" + + "67656e74346375726c2f372e31362e33" + + "206c69626375726c2f372e31362e3320" + + "4f70656e53534c2f302e392e376c207a" + + "6c69622f312e322e3304686f73740f77" + + "77772e6578616d706c652e636f6d0f61" + + "63636570742d6c616e67756167650665" + + "6e2c206d690000" + ); + deepEqual(encoded, expected); + + let mismatchedHeaders = new BinaryHttpRequest( + "GET", + "https", + "", + "", + ["whoops-only-one-header-name"], + ["some-header-value", "some-other-header-value"], + [] + ); + // The implementation uses "NS_ERROR_INVALID_ARG", because that's an + // appropriate description for the error. However, that is an alias to + // "NS_ERROR_ILLEGAL_VALUE", which is what the actual exception uses, so + // that's what is tested for here. + Assert.throws( + () => bhttp.encodeRequest(mismatchedHeaders), + /NS_ERROR_ILLEGAL_VALUE/ + ); +} + +function test_decode_request() { + let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService( + Ci.nsIBinaryHttp + ); + + // From RFC 9292. + let encoded = hexStringToBytes( + "0003474554056874747073000a2f6865" + + "6c6c6f2e747874406c0a757365722d61" + + "67656e74346375726c2f372e31362e33" + + "206c69626375726c2f372e31362e3320" + + "4f70656e53534c2f302e392e376c207a" + + "6c69622f312e322e3304686f73740f77" + + "77772e6578616d706c652e636f6d0f61" + + "63636570742d6c616e67756167650665" + + "6e2c206d690000" + ); + let request = bhttp.decodeRequest(encoded); + equal(request.method, "GET"); + equal(request.scheme, "https"); + equal(request.authority, ""); + equal(request.path, "/hello.txt"); + let expectedHeaderNames = ["user-agent", "host", "accept-language"]; + deepEqual(request.headerNames, expectedHeaderNames); + let expectedHeaderValues = [ + "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3", + "www.example.com", + "en, mi", + ]; + deepEqual(request.headerValues, expectedHeaderValues); + deepEqual(request.content, []); + + let garbage = hexStringToBytes("115f00ab64c0fa783fe4cb723eaa87fa78900a0b00"); + Assert.throws(() => bhttp.decodeRequest(garbage), /NS_ERROR_UNEXPECTED/); +} + +function test_decode_response() { + let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService( + Ci.nsIBinaryHttp + ); + // From RFC 9292. + let encoded = hexStringToBytes( + "0340660772756e6e696e670a22736c65" + + "657020313522004067046c696e6b233c" + + "2f7374796c652e6373733e3b2072656c" + + "3d7072656c6f61643b2061733d737479" + + "6c65046c696e6b243c2f736372697074" + + "2e6a733e3b2072656c3d7072656c6f61" + + "643b2061733d7363726970740040c804" + + "646174651d4d6f6e2c203237204a756c" + + "20323030392031323a32383a35332047" + + "4d540673657276657206417061636865" + + "0d6c6173742d6d6f6469666965641d57" + + "65642c203232204a756c203230303920" + + "31393a31353a353620474d5404657461" + + "671422333461613338372d642d313536" + + "3865623030220d6163636570742d7261" + + "6e6765730562797465730e636f6e7465" + + "6e742d6c656e67746802353104766172" + + "790f4163636570742d456e636f64696e" + + "670c636f6e74656e742d747970650a74" + + "6578742f706c61696e003348656c6c6f" + + "20576f726c6421204d7920636f6e7465" + + "6e7420696e636c756465732061207472" + + "61696c696e672043524c462e0d0a0000" + ); + let response = bhttp.decodeResponse(encoded); + equal(response.status, 200); + deepEqual( + response.content, + stringToBytes("Hello World! My content includes a trailing CRLF.\r\n") + ); + let expectedHeaderNames = [ + "date", + "server", + "last-modified", + "etag", + "accept-ranges", + "content-length", + "vary", + "content-type", + ]; + deepEqual(response.headerNames, expectedHeaderNames); + let expectedHeaderValues = [ + "Mon, 27 Jul 2009 12:28:53 GMT", + "Apache", + "Wed, 22 Jul 2009 19:15:56 GMT", + '"34aa387-d-1568eb00"', + "bytes", + "51", + "Accept-Encoding", + "text/plain", + ]; + deepEqual(response.headerValues, expectedHeaderValues); + + let garbage = hexStringToBytes( + "0367890084cb0ab03115fa0b4c2ea0fa783f7a87fa00" + ); + Assert.throws(() => bhttp.decodeResponse(garbage), /NS_ERROR_UNEXPECTED/); +} + +function test_encode_response() { + let response = new BinaryHttpResponse( + 418, + ["content-type"], + ["text/plain"], + stringToBytes("I'm a teapot") + ); + let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService( + Ci.nsIBinaryHttp + ); + let encoded = bhttp.encodeResponse(response); + let expected = hexStringToBytes( + "0141a2180c636f6e74656e742d747970650a746578742f706c61696e0c49276d206120746561706f7400" + ); + deepEqual(encoded, expected); + + let mismatchedHeaders = new BinaryHttpResponse( + 500, + ["some-header", "some-other-header"], + ["whoops-only-one-header-value"], + [] + ); + // The implementation uses "NS_ERROR_INVALID_ARG", because that's an + // appropriate description for the error. However, that is an alias to + // "NS_ERROR_ILLEGAL_VALUE", which is what the actual exception uses, so + // that's what is tested for here. + Assert.throws( + () => bhttp.encodeResponse(mismatchedHeaders), + /NS_ERROR_ILLEGAL_VALUE/ + ); +} + +function run_test() { + test_encode_request(); + test_decode_request(); + test_encode_response(); + test_decode_response(); +} diff --git a/netwerk/test/unit/test_blob_channelname.js b/netwerk/test/unit/test_blob_channelname.js new file mode 100644 index 0000000000..c1a09272da --- /dev/null +++ b/netwerk/test/unit/test_blob_channelname.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function channelname() { + var file = new File( + [new Blob(["test"], { type: "text/plain" })], + "test-name" + ); + var url = URL.createObjectURL(file); + var channel = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + + let inputStream = channel.open(); + ok(inputStream, "Should be able to open channel"); + ok( + inputStream.QueryInterface(Ci.nsIAsyncInputStream), + "Stream should support async operations" + ); + + await new Promise(resolve => { + inputStream.asyncWait( + () => { + let available = inputStream.available(); + ok(available, "There should be data to read"); + Assert.equal( + channel.contentDispositionFilename, + "test-name", + "filename matches" + ); + resolve(); + }, + 0, + 0, + Services.tm.mainThread + ); + }); + + inputStream.close(); + channel.cancel(Cr.NS_ERROR_FAILURE); +}); diff --git a/netwerk/test/unit/test_brotli_decoding.js b/netwerk/test/unit/test_brotli_decoding.js new file mode 100644 index 0000000000..aef7dabafe --- /dev/null +++ b/netwerk/test/unit/test_brotli_decoding.js @@ -0,0 +1,128 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ + +let endChunk2ReceivedInTime = false; + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let channelListener = function (closure) { + this._closure = closure; + this._start = Date.now(); +}; + +channelListener.prototype = { + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + let data = read_stream(stream, cnt); + let current = Date.now(); + let elapsed = current - this._start; + dump("data:" + data.slice(-10) + "\n"); + dump("elapsed=" + elapsed + "\n"); + if (elapsed < 2500 && data[data.length - 1] == "E") { + endChunk2ReceivedInTime = true; + } + }, + + onStopRequest: function testOnStopRequest(request, status) { + this._closure(); + }, +}; + +add_task(async function test_http2() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + let server = new NodeHTTP2Server(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + let chan = makeChan(`https://localhost:${server.port()}/test`); + let req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 404); + await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200, { + "content-type": "text/html; charset=utf-8", + "content-encoding": "br", + }); + resp.write( + Buffer.from([ + 0x8b, 0x2a, 0x80, 0x3c, 0x64, 0x69, 0x76, 0x3e, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x3c, 0x2f, 0x64, 0x69, 0x76, 0x3e, 0x3c, 0x64, 0x69, 0x76, 0x20, + 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x22, 0x74, 0x65, 0x78, 0x74, 0x2d, + 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x3a, 0x20, 0x65, 0x6c, + 0x6c, 0x69, 0x70, 0x73, 0x69, 0x73, 0x3b, 0x6f, 0x76, 0x65, 0x72, 0x66, + 0x6c, 0x6f, 0x77, 0x3a, 0x20, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x3b, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x72, + 0x74, 0x6c, 0x3b, 0x22, 0x3e, + ]) + ); + + // This function is handled within the httpserver where setTimeout is + // available. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout, no-undef + setTimeout(function () { + resp.write( + Buffer.from([ + 0xfa, 0xff, 0x0b, 0x00, 0x80, 0xaa, 0xaa, 0xaa, 0xea, 0x3f, 0x72, + 0x59, 0xd6, 0x05, 0x73, 0x5b, 0xb6, 0x75, 0xea, 0xe6, 0xfd, 0xa8, + 0x54, 0xc7, 0x62, 0xd8, 0x18, 0x86, 0x61, 0x18, 0x86, 0x63, 0xa9, + 0x86, 0x61, 0x18, 0x86, 0xe1, 0x63, 0x8e, 0x63, 0xa9, 0x86, 0x61, + 0x18, 0x86, 0x61, 0xd8, 0xe0, 0xbc, 0x85, 0x48, 0x1f, 0xa0, 0x05, + 0xda, 0x6f, 0xef, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0xaa, 0xaa, + 0xaa, 0xaa, 0xff, 0xc8, 0x65, 0x59, 0x17, 0xcc, 0x6d, 0xd9, 0xd6, + 0xa9, 0x9b, 0xf7, 0xff, 0x0d, 0xd5, 0xb1, 0x18, 0xe6, 0x63, 0x18, + 0x86, 0x61, 0x18, 0x8e, 0xa5, 0x1a, 0x86, 0x61, 0x18, 0x86, 0x61, + 0x8e, 0xed, 0x57, 0x0d, 0xc3, 0x30, 0x0c, 0xc3, 0xb0, 0xc1, 0x79, + 0x0b, 0x91, 0x3e, 0x40, 0x6c, 0x1c, 0x00, 0x90, 0xd6, 0x3b, 0x00, + 0x50, 0x96, 0x31, 0x53, 0xe6, 0x2c, 0x59, 0xb3, 0x65, 0xcf, 0x91, + 0x33, 0x57, 0x31, 0x03, + ]) + ); + }, 100); + + // This function is handled within the httpserver where setTimeout is + // available. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout, no-undef + setTimeout(function () { + resp.end( + Buffer.from([ + 0x98, 0x00, 0x08, 0x3c, 0x2f, 0x64, 0x69, 0x76, 0x3e, 0x3c, 0x64, + 0x69, 0x76, 0x3e, 0x65, 0x6e, 0x64, 0x3c, 0x2f, 0x64, 0x69, 0x76, + 0x3e, 0x03, + ]) + ); + }, 2500); + }); + chan = makeChan(`https://localhost:${server.port()}/test`); + await new Promise(resolve => { + chan.asyncOpen(new channelListener(() => resolve())); + }); + + equal( + endChunk2ReceivedInTime, + true, + "End of chunk 2 not received before chunk 3 was sent" + ); +}); diff --git a/netwerk/test/unit/test_brotli_http.js b/netwerk/test/unit/test_brotli_http.js new file mode 100644 index 0000000000..d25e82083a --- /dev/null +++ b/netwerk/test/unit/test_brotli_http.js @@ -0,0 +1,120 @@ +// This test exists mostly as documentation that +// Firefox can load brotli files over HTTP if we set the proper pref. + +"use strict"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Encoding", "br", false); + response.write("\x0b\x02\x80hello\x03"); +} + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +var httpServer = null; + +add_task(async function check_brotli() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + async function test() { + let chan = NetUtil.newChannel({ uri: URL, loadUsingSystemPrincipal: true }); + let [, buff] = await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + (req, buff) => { + resolve([req, buff]); + }, + null, + CL_IGNORE_CL + ) + ); + }); + return buff; + } + + Services.prefs.setBoolPref( + "network.http.encoding.trustworthy_is_https", + true + ); + equal( + await test(), + "hello", + "Should decode brotli when trustworthy_is_https=true" + ); + Services.prefs.setBoolPref( + "network.http.encoding.trustworthy_is_https", + false + ); + equal( + await test(), + "\x0b\x02\x80hello\x03", + "Should not decode brotli when trustworthy_is_https=false" + ); + Services.prefs.setCharPref( + "network.http.accept-encoding", + "gzip, deflate, br" + ); + equal( + await test(), + "hello", + "Should decode brotli if we set the HTTP accept encoding to include brotli" + ); + Services.prefs.clearUserPref("network.http.accept-encoding"); + Services.prefs.clearUserPref("network.http.encoding.trustworthy_is_https"); + await httpServer.stop(); +}); + +// Make sure we still decode brotli on HTTPS +// Node server doesn't work on Android yet. +add_task( + { skip_if: () => AppConstants.platform == "android" }, + async function check_https() { + Services.prefs.setBoolPref( + "network.http.encoding.trustworthy_is_https", + true + ); + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + let server = new NodeHTTPSServer(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + await server.registerPathHandler("/brotli", (req, resp) => { + resp.setHeader("Content-Type", "text/plain"); + resp.setHeader("Content-Encoding", "br"); + let output = "\x0b\x02\x80hello\x03"; + resp.writeHead(200); + resp.end(output, "binary"); + }); + equal( + Services.prefs.getCharPref("network.http.accept-encoding.secure"), + "gzip, deflate, br" + ); + let { req, buff } = await new Promise(resolve => { + let chan = NetUtil.newChannel({ + uri: `${server.origin()}/brotli`, + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen( + new ChannelListener( + (req, buff) => resolve({ req, buff }), + null, + CL_ALLOW_UNKNOWN_CL + ) + ); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + equal(buff, "hello"); + } +); diff --git a/netwerk/test/unit/test_bug1064258.js b/netwerk/test/unit/test_bug1064258.js new file mode 100644 index 0000000000..cc0f9fa852 --- /dev/null +++ b/netwerk/test/unit/test_bug1064258.js @@ -0,0 +1,142 @@ +/** + * Check how nsICachingChannel.cacheOnlyMetadata works. + * - all channels involved in this test are set cacheOnlyMetadata = true + * - do a previously uncached request for a long living content + * - check we have downloaded the content from the server (channel provides it) + * - check the entry has metadata, but zero-length content + * - load the same URL again, now cached + * - check the channel is giving no content (no call to OnDataAvailable) but succeeds + * - repeat again, but for a different URL that is not cached (immediately expires) + * - only difference is that we get a newer version of the content from the server during the second request + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody1 = "response body 1"; +const responseBody2a = "response body 2a"; +const responseBody2b = "response body 2b"; + +function contentHandler1(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-control", "max-age=999999"); + response.bodyOutputStream.write(responseBody1, responseBody1.length); +} + +var content2passCount = 0; + +function contentHandler2(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-control", "no-cache"); + switch (content2passCount++) { + case 0: + response.setHeader("ETag", "testetag"); + response.bodyOutputStream.write(responseBody2a, responseBody2a.length); + break; + case 1: + Assert.ok(metadata.hasHeader("If-None-Match")); + Assert.equal(metadata.getHeader("If-None-Match"), "testetag"); + response.bodyOutputStream.write(responseBody2b, responseBody2b.length); + break; + default: + throw new Error("Unexpected request in the test"); + } +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content1", contentHandler1); + httpServer.registerPathHandler("/content2", contentHandler2); + httpServer.start(-1); + + run_test_content1a(); + do_test_pending(); +} + +function run_test_content1a() { + var chan = make_channel(URL + "/content1"); + let caching = chan.QueryInterface(Ci.nsICachingChannel); + caching.cacheOnlyMetadata = true; + chan.asyncOpen(new ChannelListener(contentListener1a, null)); +} + +function contentListener1a(request, buffer) { + Assert.equal(buffer, responseBody1); + + asyncOpenCacheEntry(URL + "/content1", "disk", 0, null, cacheCheck1); +} + +function cacheCheck1(status, entry) { + Assert.equal(status, 0); + Assert.equal(entry.dataSize, 0); + try { + Assert.notEqual(entry.getMetaDataElement("response-head"), null); + } catch (ex) { + do_throw("Missing response head"); + } + + var chan = make_channel(URL + "/content1"); + let caching = chan.QueryInterface(Ci.nsICachingChannel); + caching.cacheOnlyMetadata = true; + chan.asyncOpen(new ChannelListener(contentListener1b, null, CL_IGNORE_CL)); +} + +function contentListener1b(request, buffer) { + request.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(request.requestMethod, "GET"); + Assert.equal(request.responseStatus, 200); + Assert.equal(request.getResponseHeader("Cache-control"), "max-age=999999"); + + Assert.equal(buffer, ""); + run_test_content2a(); +} + +// Now same set of steps but this time for an immediately expiring content. + +function run_test_content2a() { + var chan = make_channel(URL + "/content2"); + let caching = chan.QueryInterface(Ci.nsICachingChannel); + caching.cacheOnlyMetadata = true; + chan.asyncOpen(new ChannelListener(contentListener2a, null)); +} + +function contentListener2a(request, buffer) { + Assert.equal(buffer, responseBody2a); + + asyncOpenCacheEntry(URL + "/content2", "disk", 0, null, cacheCheck2); +} + +function cacheCheck2(status, entry) { + Assert.equal(status, 0); + Assert.equal(entry.dataSize, 0); + try { + Assert.notEqual(entry.getMetaDataElement("response-head"), null); + Assert.ok( + entry.getMetaDataElement("response-head").match("etag: testetag") + ); + } catch (ex) { + do_throw("Missing response head"); + } + + var chan = make_channel(URL + "/content2"); + let caching = chan.QueryInterface(Ci.nsICachingChannel); + caching.cacheOnlyMetadata = true; + chan.asyncOpen(new ChannelListener(contentListener2b, null)); +} + +function contentListener2b(request, buffer) { + Assert.equal(buffer, responseBody2b); + + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_bug1177909.js b/netwerk/test/unit/test_bug1177909.js new file mode 100644 index 0000000000..4b56e58f05 --- /dev/null +++ b/netwerk/test/unit/test_bug1177909.js @@ -0,0 +1,251 @@ +"use strict"; + +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gProxyService", + "@mozilla.org/network/protocol-proxy-service;1", + "nsIProtocolProxyService" +); + +XPCOMUtils.defineLazyGetter(this, "systemSettings", function () { + return { + QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]), + + mainThreadOnly: true, + PACURI: null, + + getProxyForURI(aSpec, aScheme, aHost, aPort) { + if (aPort != -1) { + return "SOCKS5 http://localhost:9050"; + } + if (aScheme == "http") { + return "PROXY http://localhost:8080"; + } + if (aScheme == "https") { + return "HTTPS https://localhost:8080"; + } + return "DIRECT"; + }, + }; +}); + +let gMockProxy = MockRegistrar.register( + "@mozilla.org/system-proxy-settings;1", + systemSettings +); + +registerCleanupFunction(() => { + MockRegistrar.unregister(gMockProxy); +}); + +function makeChannel(uri) { + return NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); +} + +async function TestProxyType(chan, flags) { + Services.prefs.setIntPref( + "network.proxy.type", + Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM + ); + + return new Promise((resolve, reject) => { + gProxyService.asyncResolve(chan, flags, { + onProxyAvailable(req, uri, pi, status) { + resolve(pi); + }, + }); + }); +} + +async function TestProxyTypeByURI(uri) { + return TestProxyType(makeChannel(uri), 0); +} + +add_task(async function testHttpProxy() { + let pi = await TestProxyTypeByURI("http://www.mozilla.org/"); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 8080, "Expected proxy port to be 8080"); + equal(pi.type, "http", "Expected proxy type to be http"); +}); + +add_task(async function testHttpsProxy() { + let pi = await TestProxyTypeByURI("https://www.mozilla.org/"); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 8080, "Expected proxy port to be 8080"); + equal(pi.type, "https", "Expected proxy type to be https"); +}); + +add_task(async function testSocksProxy() { + let pi = await TestProxyTypeByURI("http://www.mozilla.org:1234/"); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 9050, "Expected proxy port to be 8080"); + equal(pi.type, "socks", "Expected proxy type to be http"); +}); + +add_task(async function testDirectProxy() { + // Do what |WebSocketChannel::AsyncOpen| do, but do not prefer https proxy. + let proxyURI = Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec("wss://ws.mozilla.org/") + .finalize(); + let uri = proxyURI.mutate().setScheme("https").finalize(); + + let chan = Services.io.newChannelFromURIWithProxyFlags( + uri, + proxyURI, + 0, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + + let pi = await TestProxyType(chan, 0); + equal(pi, null, "Expected proxy host to be null"); +}); + +add_task(async function testWebSocketProxy() { + // Do what |WebSocketChannel::AsyncOpen| do + let proxyURI = Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec("wss://ws.mozilla.org/") + .finalize(); + let uri = proxyURI.mutate().setScheme("https").finalize(); + + let proxyFlags = + Ci.nsIProtocolProxyService.RESOLVE_PREFER_SOCKS_PROXY | + Ci.nsIProtocolProxyService.RESOLVE_PREFER_HTTPS_PROXY | + Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL; + + let chan = Services.io.newChannelFromURIWithProxyFlags( + uri, + proxyURI, + proxyFlags, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + + let pi = await TestProxyType(chan, proxyFlags); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 8080, "Expected proxy port to be 8080"); + equal(pi.type, "https", "Expected proxy type to be https"); +}); + +add_task(async function testPreferHttpsProxy() { + let uri = Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec("http://mozilla.org/") + .finalize(); + let proxyFlags = Ci.nsIProtocolProxyService.RESOLVE_PREFER_HTTPS_PROXY; + + let chan = Services.io.newChannelFromURIWithProxyFlags( + uri, + null, + proxyFlags, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + + let pi = await TestProxyType(chan, proxyFlags); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 8080, "Expected proxy port to be 8080"); + equal(pi.type, "https", "Expected proxy type to be https"); +}); + +add_task(async function testProxyHttpsToHttpIsBlocked() { + // Ensure that regressions of bug 1702417 will be detected by the next test + const turnUri = Services.io.newURI("http://turn.example.com/"); + const proxyFlags = + Ci.nsIProtocolProxyService.RESOLVE_PREFER_HTTPS_PROXY | + Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL; + + const fakeContentPrincipal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://example.com" + ); + + const chan = Services.io.newChannelFromURIWithProxyFlags( + turnUri, + null, + proxyFlags, + null, + fakeContentPrincipal, + fakeContentPrincipal, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + + const pi = await TestProxyType(chan, proxyFlags); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 8080, "Expected proxy port to be 8080"); + equal(pi.type, "https", "Expected proxy type to be https"); + + const csm = Cc["@mozilla.org/contentsecuritymanager;1"].getService( + Ci.nsIContentSecurityManager + ); + + try { + csm.performSecurityCheck(chan, null); + Assert.ok( + false, + "performSecurityCheck should fail (due to mixed content blocking)" + ); + } catch (e) { + Assert.equal( + e.result, + Cr.NS_ERROR_CONTENT_BLOCKED, + "performSecurityCheck should throw NS_ERROR_CONTENT_BLOCKED" + ); + } +}); + +add_task(async function testProxyHttpsToTurnTcpWorks() { + // Test for bug 1702417 + const turnUri = Services.io.newURI("http://turn.example.com/"); + const proxyFlags = + Ci.nsIProtocolProxyService.RESOLVE_PREFER_HTTPS_PROXY | + Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL; + + const fakeContentPrincipal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://example.com" + ); + + const chan = Services.io.newChannelFromURIWithProxyFlags( + turnUri, + null, + proxyFlags, + null, + fakeContentPrincipal, + fakeContentPrincipal, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + // This is what allows this to avoid mixed content blocking + Ci.nsIContentPolicy.TYPE_PROXIED_WEBRTC_MEDIA + ); + + const pi = await TestProxyType(chan, proxyFlags); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 8080, "Expected proxy port to be 8080"); + equal(pi.type, "https", "Expected proxy type to be https"); + + const csm = Cc["@mozilla.org/contentsecuritymanager;1"].getService( + Ci.nsIContentSecurityManager + ); + + csm.performSecurityCheck(chan, null); + Assert.ok(true, "performSecurityCheck should succeed"); +}); diff --git a/netwerk/test/unit/test_bug1195415.js b/netwerk/test/unit/test_bug1195415.js new file mode 100644 index 0000000000..eb312d27be --- /dev/null +++ b/netwerk/test/unit/test_bug1195415.js @@ -0,0 +1,116 @@ +// Test for bug 1195415 + +"use strict"; + +function run_test() { + var ios = Services.io; + var ssm = Services.scriptSecurityManager; + + // NON-UNICODE + var uri = ios.newURI("http://foo.com/file.txt"); + Assert.equal(uri.asciiHostPort, "foo.com"); + uri = uri.mutate().setPort(90).finalize(); + var prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "foo.com:90"); + Assert.equal(prin.origin, "http://foo.com:90"); + + uri = ios.newURI("http://foo.com:10/file.txt"); + Assert.equal(uri.asciiHostPort, "foo.com:10"); + uri = uri.mutate().setPort(500).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "foo.com:500"); + Assert.equal(prin.origin, "http://foo.com:500"); + + uri = ios.newURI("http://foo.com:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "foo.com:5000"); + uri = uri.mutate().setPort(20).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "foo.com:20"); + Assert.equal(prin.origin, "http://foo.com:20"); + + uri = ios.newURI("http://foo.com:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "foo.com:5000"); + uri = uri.mutate().setPort(-1).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "foo.com"); + Assert.equal(prin.origin, "http://foo.com"); + + uri = ios.newURI("http://foo.com:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "foo.com:5000"); + uri = uri.mutate().setPort(80).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "foo.com"); + Assert.equal(prin.origin, "http://foo.com"); + + // UNICODE + uri = ios.newURI("http://jos\u00e9.example.net.ch/file.txt"); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch"); + uri = uri.mutate().setPort(90).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:90"); + Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch:90"); + + uri = ios.newURI("http://jos\u00e9.example.net.ch:10/file.txt"); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:10"); + uri = uri.mutate().setPort(500).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:500"); + Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch:500"); + + uri = ios.newURI("http://jos\u00e9.example.net.ch:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:5000"); + uri = uri.mutate().setPort(20).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:20"); + Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch:20"); + + uri = ios.newURI("http://jos\u00e9.example.net.ch:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:5000"); + uri = uri.mutate().setPort(-1).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch"); + Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch"); + + uri = ios.newURI("http://jos\u00e9.example.net.ch:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:5000"); + uri = uri.mutate().setPort(80).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch"); + Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch"); + + // ipv6 + uri = ios.newURI("http://[123:45::678]/file.txt"); + Assert.equal(uri.asciiHostPort, "[123:45::678]"); + uri = uri.mutate().setPort(90).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "[123:45::678]:90"); + Assert.equal(prin.origin, "http://[123:45::678]:90"); + + uri = ios.newURI("http://[123:45::678]:10/file.txt"); + Assert.equal(uri.asciiHostPort, "[123:45::678]:10"); + uri = uri.mutate().setPort(500).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "[123:45::678]:500"); + Assert.equal(prin.origin, "http://[123:45::678]:500"); + + uri = ios.newURI("http://[123:45::678]:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "[123:45::678]:5000"); + uri = uri.mutate().setPort(20).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "[123:45::678]:20"); + Assert.equal(prin.origin, "http://[123:45::678]:20"); + + uri = ios.newURI("http://[123:45::678]:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "[123:45::678]:5000"); + uri = uri.mutate().setPort(-1).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "[123:45::678]"); + Assert.equal(prin.origin, "http://[123:45::678]"); + + uri = ios.newURI("http://[123:45::678]:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "[123:45::678]:5000"); + uri = uri.mutate().setPort(80).finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "[123:45::678]"); + Assert.equal(prin.origin, "http://[123:45::678]"); +} diff --git a/netwerk/test/unit/test_bug1218029.js b/netwerk/test/unit/test_bug1218029.js new file mode 100644 index 0000000000..48165807bf --- /dev/null +++ b/netwerk/test/unit/test_bug1218029.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var tests = [ + { data: "", chunks: [], status: Cr.NS_OK, consume: [], dataChunks: [""] }, + { + data: "TWO-PARTS", + chunks: [4, 5], + status: Cr.NS_OK, + consume: [4, 5], + dataChunks: ["TWO-", "PARTS", ""], + }, + { + data: "TWO-PARTS", + chunks: [4, 5], + status: Cr.NS_OK, + consume: [0, 0], + dataChunks: ["TWO-", "TWO-PARTS", "TWO-PARTS"], + }, + { + data: "3-PARTS", + chunks: [1, 1, 5], + status: Cr.NS_OK, + consume: [0, 2, 5], + dataChunks: ["3", "3-", "PARTS", ""], + }, + { + data: "ALL-AT-ONCE", + chunks: [11], + status: Cr.NS_OK, + consume: [0], + dataChunks: ["ALL-AT-ONCE", "ALL-AT-ONCE"], + }, + { + data: "ALL-AT-ONCE", + chunks: [11], + status: Cr.NS_OK, + consume: [11], + dataChunks: ["ALL-AT-ONCE", ""], + }, + { + data: "ERROR", + chunks: [1], + status: Cr.NS_ERROR_OUT_OF_MEMORY, + consume: [0], + dataChunks: ["E", "E"], + }, +]; + +/** + * @typedef TestData + * @property {string} data - data for the test. + * @property {Array} chunks - lengths of the chunks that are incrementally sent + * to the loader. + * @property {number} status - final status sent on onStopRequest. + * @property {Array} consume - lengths of consumed data that is reported at + * the onIncrementalData callback. + * @property {Array} dataChunks - data chunks that are reported at the + * onIncrementalData and onStreamComplete callbacks. + */ + +function execute_test(test) { + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = test.data; + + let channel = { + contentLength: -1, + QueryInterface: ChromeUtils.generateQI(["nsIChannel"]), + }; + + let chunkIndex = 0; + + let observer = { + onStreamComplete(loader, context, status, length, data) { + equal(chunkIndex, test.dataChunks.length - 1); + var expectedChunk = test.dataChunks[chunkIndex]; + equal(length, expectedChunk.length); + equal(String.fromCharCode.apply(null, data), expectedChunk); + + equal(status, test.status); + }, + onIncrementalData(loader, context, length, data, consumed) { + ok(chunkIndex < test.dataChunks.length - 1); + var expectedChunk = test.dataChunks[chunkIndex]; + equal(length, expectedChunk.length); + equal(String.fromCharCode.apply(null, data), expectedChunk); + + consumed.value = test.consume[chunkIndex]; + chunkIndex++; + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIIncrementalStreamLoaderObserver", + ]), + }; + + let listener = Cc[ + "@mozilla.org/network/incremental-stream-loader;1" + ].createInstance(Ci.nsIIncrementalStreamLoader); + listener.init(observer); + + listener.onStartRequest(channel); + var offset = 0; + test.chunks.forEach(function (chunkLength) { + listener.onDataAvailable(channel, stream, offset, chunkLength); + offset += chunkLength; + }); + listener.onStopRequest(channel, test.status); +} + +function run_test() { + tests.forEach(execute_test); +} diff --git a/netwerk/test/unit/test_bug1279246.js b/netwerk/test/unit/test_bug1279246.js new file mode 100644 index 0000000000..00e6f169bf --- /dev/null +++ b/netwerk/test/unit/test_bug1279246.js @@ -0,0 +1,98 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var pass = 0; +var responseBody = [0x0b, 0x02, 0x80, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x03]; +var responseLen = 5; +var testUrl = "/test/brotli"; + +function setupChannel() { + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + testUrl, + loadUsingSystemPrincipal: true, + }); +} + +function Listener() {} + +Listener.prototype = { + _buffer: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + Assert.equal(request.status, Cr.NS_OK); + this._buffer = ""; + }, + + onDataAvailable(request, stream, offset, cnt) { + if (pass == 0) { + this._buffer = this._buffer.concat(read_stream(stream, cnt)); + } else { + request.QueryInterface(Ci.nsICachingChannel); + if (!request.isFromCache()) { + do_throw("Response is not from the cache"); + } + + request.cancel(Cr.NS_ERROR_ABORT); + } + }, + + onStopRequest(request, status) { + if (pass == 0) { + Assert.equal(this._buffer.length, responseLen); + pass++; + + var channel = setupChannel(); + channel.loadFlags = Ci.nsIRequest.VALIDATE_NEVER; + channel.asyncOpen(new Listener()); + } else { + httpserver.stop(do_test_finished); + prefs.setCharPref("network.http.accept-encoding", cePref); + } + }, +}; + +var prefs; +var cePref; +function run_test() { + do_get_profile(); + + prefs = Services.prefs; + cePref = prefs.getCharPref("network.http.accept-encoding"); + prefs.setCharPref("network.http.accept-encoding", "gzip, deflate, br"); + + // Disable rcwn to make cache behavior deterministic. + prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpserver.registerPathHandler(testUrl, handler); + httpserver.start(-1); + + var channel = setupChannel(); + channel.asyncOpen(new Listener()); + + do_test_pending(); +} + +function handler(metadata, response) { + Assert.equal(pass, 0); // the second response must be server from the cache + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Encoding", "br", false); + response.setHeader("Content-Length", "" + responseBody.length, false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + bos.setOutputStream(response.bodyOutputStream); + + response.processAsync(); + bos.writeByteArray(responseBody); + response.finish(); +} diff --git a/netwerk/test/unit/test_bug1312774_http1.js b/netwerk/test/unit/test_bug1312774_http1.js new file mode 100644 index 0000000000..9c13441d4d --- /dev/null +++ b/netwerk/test/unit/test_bug1312774_http1.js @@ -0,0 +1,147 @@ +// test bug 1312774. +// Create 6 (=network.http.max-persistent-connections-per-server) +// common Http requests and 2 urgent-start Http requests to a single +// host and path, in parallel. +// Let all the requests unanswered by the server handler. (process them +// async and don't finish) +// The first 6 pending common requests will fill the limit for per-server +// parallelism. +// But the two urgent requests must reach the server despite those 6 common +// pending requests. +// The server handler doesn't let the test finish until all 8 expected requests +// arrive. +// Note: if the urgent request handling is broken (the urgent-marked requests +// get blocked by queuing) this test will time out + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +var server = new HttpServer(); +server.start(-1); +var baseURL = "http://localhost:" + server.identity.primaryPort + "/"; +var maxConnections = 0; +var urgentRequests = 0; +var debug = false; + +function log(msg) { + if (!debug) { + return; + } + + if (msg) { + dump("TEST INFO | " + msg + "\n"); + } +} + +function make_channel(url) { + var request = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + request.QueryInterface(Ci.nsIHttpChannel); + return request; +} + +function serverStopListener() { + server.stop(); +} + +function commonHttpRequest(id) { + let uri = baseURL; + var chan = make_channel(uri); + var listner = new HttpResponseListener(id); + chan.setRequestHeader("X-ID", id, false); + chan.setRequestHeader("Cache-control", "no-store", false); + chan.asyncOpen(listner); + log("Create common http request id=" + id); +} + +function urgentStartHttpRequest(id) { + let uri = baseURL; + var chan = make_channel(uri); + var listner = new HttpResponseListener(id); + var cos = chan.QueryInterface(Ci.nsIClassOfService); + cos.addClassFlags(Ci.nsIClassOfService.UrgentStart); + chan.setRequestHeader("X-ID", id, false); + chan.setRequestHeader("Cache-control", "no-store", false); + chan.asyncOpen(listner); + log("Create urgent-start http request id=" + id); +} + +function setup_httpRequests() { + log("setup_httpRequests"); + for (var i = 0; i < maxConnections; i++) { + commonHttpRequest(i); + do_test_pending(); + } +} + +function setup_urgentStartRequests() { + for (var i = 0; i < urgentRequests; i++) { + urgentStartHttpRequest(1000 + i); + do_test_pending(); + } +} + +function HttpResponseListener(id) { + this.id = id; +} + +HttpResponseListener.prototype = { + onStartRequest(request) {}, + + onDataAvailable(request, stream, off, cnt) {}, + + onStopRequest(request, status) { + log("STOP id=" + this.id); + do_test_finished(); + }, +}; + +var responseQueue = []; +function setup_http_server() { + log("setup_http_server"); + maxConnections = Services.prefs.getIntPref( + "network.http.max-persistent-connections-per-server" + ); + urgentRequests = 2; + var allCommonHttpRequestReceived = false; + // Start server; will be stopped at test cleanup time. + server.registerPathHandler("/", function (metadata, response) { + var id = metadata.getHeader("X-ID"); + log("Server recived the response id=" + id); + response.processAsync(); + responseQueue.push(response); + + if ( + responseQueue.length == maxConnections && + !allCommonHttpRequestReceived + ) { + allCommonHttpRequestReceived = true; + setup_urgentStartRequests(); + } + // Wait for all expected requests to come but don't process then. + // Collect them in a queue for later processing. We don't want to + // respond to the client until all the expected requests are made + // to the server. + if (responseQueue.length == maxConnections + urgentRequests) { + processResponse(); + } + }); + + registerCleanupFunction(function () { + server.stop(serverStopListener); + }); +} + +function processResponse() { + while (responseQueue.length) { + var resposne = responseQueue.pop(); + resposne.finish(); + } +} + +function run_test() { + setup_http_server(); + setup_httpRequests(); +} diff --git a/netwerk/test/unit/test_bug1312782_http1.js b/netwerk/test/unit/test_bug1312782_http1.js new file mode 100644 index 0000000000..eaabb92289 --- /dev/null +++ b/netwerk/test/unit/test_bug1312782_http1.js @@ -0,0 +1,195 @@ +// test bug 1312782. +// +// Summary: +// Assume we have 6 http requests in queue, 4 are from the focused window and +// the other 2 are from the non-focused window. We want to test that the server +// should receive 4 requests from the focused window first and then receive the +// rest 2 requests. +// +// Test step: +// 1. Create 6 dummy http requests. Server would not process responses until get +// all 6 requests. +// 2. Once server receive 6 dummy requests, create 4 http requests with the focused +// window id and 2 requests with non-focused window id. Note that the requets's +// id is a serial number starting from the focused window id. +// 3. Server starts to process the 6 dummy http requests, so the client can start to +// process the pending queue. Server will queue those http requests again and wait +// until get all 6 requests. +// 4. When the server receive all 6 requests, starts to check that the request ids of +// the first 4 requests in the queue should be all less than focused window id +// plus 4. Also, the request ids of the rest requests should be less than non-focused +// window id + 2. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var server = new HttpServer(); +server.start(-1); +var baseURL = "http://localhost:" + server.identity.primaryPort + "/"; +var maxConnections = 0; +var debug = false; +const FOCUSED_WINDOW_ID = 123; +var NON_FOCUSED_WINDOW_ID; +var FOCUSED_WINDOW_REQUEST_COUNT; +var NON_FOCUSED_WINDOW_REQUEST_COUNT; + +function log(msg) { + if (!debug) { + return; + } + + if (msg) { + dump("TEST INFO | " + msg + "\n"); + } +} + +function make_channel(url) { + var request = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + request.QueryInterface(Ci.nsIHttpChannel); + return request; +} + +function serverStopListener() { + server.stop(); +} + +function createHttpRequest(browserId, requestId) { + let uri = baseURL; + var chan = make_channel(uri); + chan.browserId = browserId; + var listner = new HttpResponseListener(requestId); + chan.setRequestHeader("X-ID", requestId, false); + chan.setRequestHeader("Cache-control", "no-store", false); + chan.asyncOpen(listner); + log("Create http request id=" + requestId); +} + +function setup_dummyHttpRequests() { + log("setup_dummyHttpRequests"); + for (var i = 0; i < maxConnections; i++) { + createHttpRequest(0, i); + do_test_pending(); + } +} + +function setup_focusedWindowHttpRequests() { + log("setup_focusedWindowHttpRequests"); + for (var i = 0; i < FOCUSED_WINDOW_REQUEST_COUNT; i++) { + createHttpRequest(FOCUSED_WINDOW_ID, FOCUSED_WINDOW_ID + i); + do_test_pending(); + } +} + +function setup_nonFocusedWindowHttpRequests() { + log("setup_nonFocusedWindowHttpRequests"); + for (var i = 0; i < NON_FOCUSED_WINDOW_REQUEST_COUNT; i++) { + createHttpRequest(NON_FOCUSED_WINDOW_ID, NON_FOCUSED_WINDOW_ID + i); + do_test_pending(); + } +} + +function HttpResponseListener(id) { + this.id = id; +} + +HttpResponseListener.prototype = { + onStartRequest(request) {}, + + onDataAvailable(request, stream, off, cnt) {}, + + onStopRequest(request, status) { + log("STOP id=" + this.id); + do_test_finished(); + }, +}; + +function check_response_id(responses, maxWindowId) { + for (var i = 0; i < responses.length; i++) { + var id = responses[i].getHeader("X-ID"); + log("response id=" + id + " maxWindowId=" + maxWindowId); + Assert.ok(id < maxWindowId); + } +} + +var responseQueue = []; +function setup_http_server() { + log("setup_http_server"); + maxConnections = Services.prefs.getIntPref( + "network.http.max-persistent-connections-per-server" + ); + FOCUSED_WINDOW_REQUEST_COUNT = Math.floor(maxConnections * 0.8); + NON_FOCUSED_WINDOW_REQUEST_COUNT = + maxConnections - FOCUSED_WINDOW_REQUEST_COUNT; + NON_FOCUSED_WINDOW_ID = FOCUSED_WINDOW_ID + FOCUSED_WINDOW_REQUEST_COUNT; + + var allDummyHttpRequestReceived = false; + // Start server; will be stopped at test cleanup time. + server.registerPathHandler("/", function (metadata, response) { + var id = metadata.getHeader("X-ID"); + log("Server recived the response id=" + id); + + response.processAsync(); + response.setHeader("X-ID", id); + responseQueue.push(response); + + if ( + responseQueue.length == maxConnections && + !allDummyHttpRequestReceived + ) { + log("received all dummy http requets"); + allDummyHttpRequestReceived = true; + setup_nonFocusedWindowHttpRequests(); + setup_focusedWindowHttpRequests(); + processResponses(); + } else if (responseQueue.length == maxConnections) { + var focusedWindowResponses = responseQueue.slice( + 0, + FOCUSED_WINDOW_REQUEST_COUNT + ); + var nonFocusedWindowResponses = responseQueue.slice( + FOCUSED_WINDOW_REQUEST_COUNT, + responseQueue.length + ); + check_response_id( + focusedWindowResponses, + FOCUSED_WINDOW_ID + FOCUSED_WINDOW_REQUEST_COUNT + ); + check_response_id( + nonFocusedWindowResponses, + NON_FOCUSED_WINDOW_ID + NON_FOCUSED_WINDOW_REQUEST_COUNT + ); + processResponses(); + } + }); + + registerCleanupFunction(function () { + server.stop(serverStopListener); + }); +} + +function processResponses() { + while (responseQueue.length) { + var resposne = responseQueue.pop(); + resposne.finish(); + } +} + +function run_test() { + // Make sure "network.http.active_tab_priority" is true, so we can expect to + // receive http requests with focused window id before others. + Services.prefs.setBoolPref("network.http.active_tab_priority", true); + + setup_http_server(); + setup_dummyHttpRequests(); + + var windowIdWrapper = Cc["@mozilla.org/supports-PRUint64;1"].createInstance( + Ci.nsISupportsPRUint64 + ); + windowIdWrapper.data = FOCUSED_WINDOW_ID; + var obsvc = Services.obs; + obsvc.notifyObservers(windowIdWrapper, "net:current-browser-id"); +} diff --git a/netwerk/test/unit/test_bug1355539_http1.js b/netwerk/test/unit/test_bug1355539_http1.js new file mode 100644 index 0000000000..7203179cd1 --- /dev/null +++ b/netwerk/test/unit/test_bug1355539_http1.js @@ -0,0 +1,204 @@ +// test bug 1355539. +// +// Summary: +// Transactions in one pending queue are splited into two groups: +// [(Blocking Group)|(Non Blocking Group)] +// In each group, the transactions are ordered by its priority. +// This test will check if the transaction's order in pending queue is correct. +// +// Test step: +// 1. Create 6 dummy http requests. Server would not process responses until get +// all 6 requests. +// 2. Once server receive 6 dummy requests, create another 6 http requests with the +// defined priority and class flag in |transactionQueue|. +// 3. Server starts to process the 6 dummy http requests, so the client can start to +// process the pending queue. Server will queue those http requests and put them in +// |responseQueue|. +// 4. When the server receive all 6 requests, check if the order in |responseQueue| is +// equal to |transactionQueue| by comparing the value of X-ID. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var server = new HttpServer(); +server.start(-1); +var baseURL = "http://localhost:" + server.identity.primaryPort + "/"; +var maxConnections = 0; +var debug = false; +var dummyResponseQueue = []; +var responseQueue = []; + +function log(msg) { + if (!debug) { + return; + } + + if (msg) { + dump("TEST INFO | " + msg + "\n"); + } +} + +function make_channel(url) { + var request = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + request.QueryInterface(Ci.nsIHttpChannel); + return request; +} + +function serverStopListener() { + server.stop(); +} + +function createHttpRequest(requestId, priority, isBlocking, callback) { + let uri = baseURL; + var chan = make_channel(uri); + var listner = new HttpResponseListener(requestId, callback); + chan.setRequestHeader("X-ID", requestId, false); + chan.setRequestHeader("Cache-control", "no-store", false); + chan.QueryInterface(Ci.nsISupportsPriority).priority = priority; + if (isBlocking) { + var cos = chan.QueryInterface(Ci.nsIClassOfService); + cos.addClassFlags(Ci.nsIClassOfService.Leader); + } + chan.asyncOpen(listner); + log("Create http request id=" + requestId); +} + +function setup_dummyHttpRequests(callback) { + log("setup_dummyHttpRequests"); + for (var i = 0; i < maxConnections; i++) { + createHttpRequest(i, i, false, callback); + do_test_pending(); + } +} + +var transactionQueue = [ + { + requestId: 101, + priority: Ci.nsISupportsPriority.PRIORITY_HIGH, + isBlocking: true, + }, + { + requestId: 102, + priority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + isBlocking: true, + }, + { + requestId: 103, + priority: Ci.nsISupportsPriority.PRIORITY_LOW, + isBlocking: true, + }, + { + requestId: 104, + priority: Ci.nsISupportsPriority.PRIORITY_HIGH, + isBlocking: false, + }, + { + requestId: 105, + priority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + isBlocking: false, + }, + { + requestId: 106, + priority: Ci.nsISupportsPriority.PRIORITY_LOW, + isBlocking: false, + }, +]; + +function setup_HttpRequests() { + log("setup_HttpRequests"); + // Create channels in reverse order + for (var i = transactionQueue.length - 1; i > -1; ) { + var e = transactionQueue[i]; + createHttpRequest(e.requestId, e.priority, e.isBlocking); + do_test_pending(); + --i; + } +} + +function check_response_id(responses) { + for (var i = 0; i < responses.length; i++) { + var id = responses[i].getHeader("X-ID"); + Assert.equal(id, transactionQueue[i].requestId); + } +} + +function HttpResponseListener(id, onStopCallback) { + this.id = id; + this.stopCallback = onStopCallback; +} + +HttpResponseListener.prototype = { + onStartRequest(request) {}, + + onDataAvailable(request, stream, off, cnt) {}, + + onStopRequest(request, status) { + log("STOP id=" + this.id); + do_test_finished(); + if (this.stopCallback) { + this.stopCallback(); + } + }, +}; + +function setup_http_server() { + log("setup_http_server"); + maxConnections = Services.prefs.getIntPref( + "network.http.max-persistent-connections-per-server" + ); + + var allDummyHttpRequestReceived = false; + // Start server; will be stopped at test cleanup time. + server.registerPathHandler("/", function (metadata, response) { + var id = metadata.getHeader("X-ID"); + log("Server recived the response id=" + id); + + response.processAsync(); + response.setHeader("X-ID", id); + + if (!allDummyHttpRequestReceived) { + dummyResponseQueue.push(response); + } else { + responseQueue.push(response); + } + + if (dummyResponseQueue.length == maxConnections) { + log("received all dummy http requets"); + allDummyHttpRequestReceived = true; + setup_HttpRequests(); + processDummyResponse(); + } else if (responseQueue.length == maxConnections) { + log("received all http requets"); + check_response_id(responseQueue); + processResponses(); + } + }); + + registerCleanupFunction(function () { + server.stop(serverStopListener); + }); +} + +function processDummyResponse() { + if (!dummyResponseQueue.length) { + return; + } + var resposne = dummyResponseQueue.pop(); + resposne.finish(); +} + +function processResponses() { + while (responseQueue.length) { + var resposne = responseQueue.pop(); + resposne.finish(); + } +} + +function run_test() { + setup_http_server(); + setup_dummyHttpRequests(processDummyResponse); +} diff --git a/netwerk/test/unit/test_bug1378385_http1.js b/netwerk/test/unit/test_bug1378385_http1.js new file mode 100644 index 0000000000..48bcb56767 --- /dev/null +++ b/netwerk/test/unit/test_bug1378385_http1.js @@ -0,0 +1,196 @@ +// test bug 1378385. +// +// Summary: +// Assume we have 6 http requests in queue, 3 are from the focused window with +// normal priority and the other 3 are from the non-focused window with the +// highest priority. +// We want to test that when "network.http.active_tab_priority" is false, +// the server should receive 3 requests with the highest priority first +// and then receive the rest 3 requests. +// +// Test step: +// 1. Create 6 dummy http requests. Server would not process responses until told +// all 6 requests. +// 2. Once server receive 6 dummy requests, create 3 http requests with the focused +// window id and normal priority and 3 requests with non-focused window id and +// the highrst priority. +// Note that the requets's id is set to its window id. +// 3. Server starts to process the 6 dummy http requests, so the client can start to +// process the pending queue. Server will queue those http requests again and wait +// until get all 6 requests. +// 4. When the server receive all 6 requests, we want to check if 3 requests with higher +// priority are sent before others. +// First, we check that if the request id of the first 3 requests in the queue is +// equal to non focused window id. +// Second, we check if the request id of the rest requests is equal to focused +// window id. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var server = new HttpServer(); +server.start(-1); +var baseURL = "http://localhost:" + server.identity.primaryPort + "/"; +var maxConnections = 0; +var debug = false; +const FOCUSED_WINDOW_ID = 123; +var NON_FOCUSED_WINDOW_ID; +var FOCUSED_WINDOW_REQUEST_COUNT; +var NON_FOCUSED_WINDOW_REQUEST_COUNT; + +function log(msg) { + if (!debug) { + return; + } + + if (msg) { + dump("TEST INFO | " + msg + "\n"); + } +} + +function make_channel(url) { + var request = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + request.QueryInterface(Ci.nsIHttpChannel); + return request; +} + +function serverStopListener() { + server.stop(); +} + +function createHttpRequest(browserId, requestId, priority) { + let uri = baseURL; + var chan = make_channel(uri); + chan.browserId = browserId; + chan.QueryInterface(Ci.nsISupportsPriority).priority = priority; + var listner = new HttpResponseListener(requestId); + chan.setRequestHeader("X-ID", requestId, false); + chan.setRequestHeader("Cache-control", "no-store", false); + chan.asyncOpen(listner); + log("Create http request id=" + requestId); +} + +function setup_dummyHttpRequests() { + log("setup_dummyHttpRequests"); + for (var i = 0; i < maxConnections; i++) { + createHttpRequest(0, i, Ci.nsISupportsPriority.PRIORITY_NORMAL); + do_test_pending(); + } +} + +function setup_focusedWindowHttpRequests() { + log("setup_focusedWindowHttpRequests"); + for (var i = 0; i < FOCUSED_WINDOW_REQUEST_COUNT; i++) { + createHttpRequest( + FOCUSED_WINDOW_ID, + FOCUSED_WINDOW_ID, + Ci.nsISupportsPriority.PRIORITY_NORMAL + ); + do_test_pending(); + } +} + +function setup_nonFocusedWindowHttpRequests() { + log("setup_nonFocusedWindowHttpRequests"); + for (var i = 0; i < NON_FOCUSED_WINDOW_REQUEST_COUNT; i++) { + createHttpRequest( + NON_FOCUSED_WINDOW_ID, + NON_FOCUSED_WINDOW_ID, + Ci.nsISupportsPriority.PRIORITY_HIGHEST + ); + do_test_pending(); + } +} + +function HttpResponseListener(id) { + this.id = id; +} + +HttpResponseListener.prototype = { + onStartRequest(request) {}, + + onDataAvailable(request, stream, off, cnt) {}, + + onStopRequest(request, status) { + log("STOP id=" + this.id); + do_test_finished(); + }, +}; + +function check_response_id(responses, browserId) { + for (var i = 0; i < responses.length; i++) { + var id = responses[i].getHeader("X-ID"); + log("response id=" + id + " browserId=" + browserId); + Assert.equal(id, browserId); + } +} + +var responseQueue = []; +function setup_http_server() { + log("setup_http_server"); + maxConnections = Services.prefs.getIntPref( + "network.http.max-persistent-connections-per-server" + ); + FOCUSED_WINDOW_REQUEST_COUNT = Math.floor(maxConnections * 0.5); + NON_FOCUSED_WINDOW_REQUEST_COUNT = + maxConnections - FOCUSED_WINDOW_REQUEST_COUNT; + NON_FOCUSED_WINDOW_ID = FOCUSED_WINDOW_ID + FOCUSED_WINDOW_REQUEST_COUNT; + + var allDummyHttpRequestReceived = false; + // Start server; will be stopped at test cleanup time. + server.registerPathHandler("/", function (metadata, response) { + var id = metadata.getHeader("X-ID"); + log("Server recived the response id=" + id); + + response.processAsync(); + response.setHeader("X-ID", id); + responseQueue.push(response); + + if ( + responseQueue.length == maxConnections && + !allDummyHttpRequestReceived + ) { + log("received all dummy http requets"); + allDummyHttpRequestReceived = true; + setup_nonFocusedWindowHttpRequests(); + setup_focusedWindowHttpRequests(); + processResponses(); + } else if (responseQueue.length == maxConnections) { + var nonFocusedWindowResponses = responseQueue.slice( + 0, + NON_FOCUSED_WINDOW_REQUEST_COUNT + ); + var focusedWindowResponses = responseQueue.slice( + NON_FOCUSED_WINDOW_REQUEST_COUNT, + responseQueue.length + ); + check_response_id(nonFocusedWindowResponses, NON_FOCUSED_WINDOW_ID); + check_response_id(focusedWindowResponses, FOCUSED_WINDOW_ID); + processResponses(); + } + }); + + registerCleanupFunction(function () { + server.stop(serverStopListener); + }); +} + +function processResponses() { + while (responseQueue.length) { + var resposne = responseQueue.pop(); + resposne.finish(); + } +} + +function run_test() { + // Set "network.http.active_tab_priority" to false, so we can expect to + // receive http requests with higher priority first. + Services.prefs.setBoolPref("network.http.active_tab_priority", false); + + setup_http_server(); + setup_dummyHttpRequests(); +} diff --git a/netwerk/test/unit/test_bug1411316_http1.js b/netwerk/test/unit/test_bug1411316_http1.js new file mode 100644 index 0000000000..ef163024d6 --- /dev/null +++ b/netwerk/test/unit/test_bug1411316_http1.js @@ -0,0 +1,114 @@ +// Test bug 1411316. +// +// Summary: +// The purpose of this test is to test whether the HttpConnectionMgr really +// cancel and close all connecitons when get "net:cancel-all-connections". +// +// Test step: +// 1. Create 6 http requests. Server would not process responses and just put +// all requests in its queue. +// 2. Once server receive all 6 requests, call notifyObservers with the +// topic "net:cancel-all-connections". +// 3. We expect that all 6 active connections should be closed with the status +// NS_ERROR_ABORT. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var server = new HttpServer(); +server.start(-1); +var baseURL = "http://localhost:" + server.identity.primaryPort + "/"; +var maxConnections = 0; +var debug = false; +var requestId = 0; + +function log(msg) { + if (!debug) { + return; + } + + if (msg) { + dump("TEST INFO | " + msg + "\n"); + } +} + +function make_channel(url) { + var request = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + request.QueryInterface(Ci.nsIHttpChannel); + return request; +} + +function serverStopListener() { + server.stop(); +} + +function createHttpRequest(status) { + let uri = baseURL; + var chan = make_channel(uri); + var listner = new HttpResponseListener(++requestId, status); + chan.setRequestHeader("X-ID", requestId, false); + chan.setRequestHeader("Cache-control", "no-store", false); + chan.asyncOpen(listner); + log("Create http request id=" + requestId); +} + +function setupHttpRequests(status) { + log("setupHttpRequests"); + for (var i = 0; i < maxConnections; i++) { + createHttpRequest(status); + do_test_pending(); + } +} + +function HttpResponseListener(id, onStopRequestStatus) { + this.id = id; + this.onStopRequestStatus = onStopRequestStatus; +} + +HttpResponseListener.prototype = { + onStartRequest(request) {}, + + onDataAvailable(request, stream, off, cnt) {}, + + onStopRequest(request, status) { + log("STOP id=" + this.id + " status=" + status); + Assert.ok(this.onStopRequestStatus == status); + do_test_finished(); + }, +}; + +var responseQueue = []; +function setup_http_server() { + log("setup_http_server"); + maxConnections = Services.prefs.getIntPref( + "network.http.max-persistent-connections-per-server" + ); + + // Start server; will be stopped at test cleanup time. + server.registerPathHandler("/", function (metadata, response) { + var id = metadata.getHeader("X-ID"); + log("Server recived the response id=" + id); + + response.processAsync(); + response.setHeader("X-ID", id); + responseQueue.push(response); + + if (responseQueue.length == maxConnections) { + log("received all http requets"); + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + } + }); + + registerCleanupFunction(function () { + server.stop(serverStopListener); + }); +} + +function run_test() { + setup_http_server(); + setupHttpRequests(Cr.NS_ERROR_ABORT); +} diff --git a/netwerk/test/unit/test_bug1527293.js b/netwerk/test/unit/test_bug1527293.js new file mode 100644 index 0000000000..0cfda1b61f --- /dev/null +++ b/netwerk/test/unit/test_bug1527293.js @@ -0,0 +1,92 @@ +// Test bug 1527293 +// +// Summary: +// The purpose of this test is to check that a cache entry is doomed and not +// reused when we don't write the content due to max entry size limit. +// +// Test step: +// 1. Create http request for an entry whose size is bigger than we allow to +// cache. The response must contain Content-Range header so the content size +// is known in advance, but it must not contain Content-Length header because +// the bug isn't reproducible with it. +// 2. After receiving and checking the content do the same request again. +// 3. Check that the request isn't conditional, i.e. the entry from previous +// load was doomed. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +// need something bigger than 1024 bytes +const responseBody = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + + Assert.throws( + () => { + metadata.getHeader("If-None-Match"); + }, + /NS_ERROR_NOT_AVAILABLE/, + "conditional request not expected" + ); + + response.setHeader("Accept-Ranges", "bytes"); + let len = responseBody.length; + response.setHeader("Content-Range", "0-" + (len - 1) + "/" + len); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + // Static check + Assert.ok(responseBody.length > 1024); + + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan = make_channel(URL + "/content"); + chan.asyncOpen(new ChannelListener(firstTimeThrough, null)); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + + var chan = make_channel(URL + "/content"); + chan.asyncOpen(new ChannelListener(secondTimeThrough, null)); +} + +function secondTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_bug1683176.js b/netwerk/test/unit/test_bug1683176.js new file mode 100644 index 0000000000..ac99e4c8ae --- /dev/null +++ b/netwerk/test/unit/test_bug1683176.js @@ -0,0 +1,89 @@ +// Test bug 1683176 +// +// Summary: +// Test the case when a channel is cancelled when "negotiate" authentication +// is processing. +// + +"use strict"; + +let prefs; +let httpserv; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +function makeChan(url, loadingUrl) { + var principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(loadingUrl), + {} + ); + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); +} + +function authHandler(metadata, response) { + var body = "blablabla"; + + response.seizePower(); + response.write("HTTP/1.1 401 Unauthorized\r\n"); + response.write("WWW-Authenticate: Negotiate\r\n"); + response.write("WWW-Authenticate: Basic realm=test\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function setup() { + prefs = Services.prefs; + + prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + prefs.setStringPref("network.negotiate-auth.trusted-uris", "localhost"); + + httpserv = new HttpServer(); + httpserv.registerPathHandler("/auth", authHandler); + httpserv.start(-1); +} + +setup(); +registerCleanupFunction(async () => { + prefs.clearUserPref("network.auth.subresource-http-auth-allow"); + prefs.clearUserPref("network.negotiate-auth.trusted-uris"); + await httpserv.stop(); +}); + +function channelOpenPromise(chan) { + return new Promise(resolve => { + let topic = "http-on-transaction-suspended-authentication"; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == topic) { + Services.obs.removeObserver(observer, topic); + let channel = aSubject.QueryInterface(Ci.nsIChannel); + channel.cancel(Cr.NS_BINDING_ABORTED); + resolve(); + } + }, + }; + Services.obs.addObserver(observer, topic); + + chan.asyncOpen(new ChannelListener(finish, null, CL_EXPECT_FAILURE)); + function finish() { + resolve(); + } + }); +} + +add_task(async function testCancelAuthentication() { + let chan = makeChan(URL + "/auth", URL); + await channelOpenPromise(chan); + Assert.equal(chan.status, Cr.NS_BINDING_ABORTED); +}); diff --git a/netwerk/test/unit/test_bug1725766.js b/netwerk/test/unit/test_bug1725766.js new file mode 100644 index 0000000000..45364d7685 --- /dev/null +++ b/netwerk/test/unit/test_bug1725766.js @@ -0,0 +1,83 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +let hserv = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); +let handlerInfo; +const testScheme = "x-moz-test"; + +function setup() { + var handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + handler.name = testScheme; + handler.uriTemplate = "http://test.mozilla.org/%s"; + + var extps = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + handlerInfo = extps.getProtocolHandlerInfo(testScheme); + handlerInfo.possibleApplicationHandlers.appendElement(handler); + + hserv.store(handlerInfo); + Assert.ok(extps.externalProtocolHandlerExists(testScheme)); +} + +setup(); +registerCleanupFunction(() => { + hserv.remove(handlerInfo); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +add_task(async function viewsourceExternalProtocol() { + Assert.throws( + () => makeChan(`view-source:${testScheme}:foo.example.com`), + /NS_ERROR_MALFORMED_URI/ + ); +}); + +add_task(async function viewsourceExternalProtocolRedirect() { + let httpserv = new HttpServer(); + httpserv.registerPathHandler("/", function handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", `${testScheme}:foo@bar.com`, false); + + var body = "Moved\n"; + response.bodyOutputStream.write(body, body.length); + }); + httpserv.start(-1); + + let chan = makeChan( + `view-source:http://127.0.0.1:${httpserv.identity.primaryPort}/` + ); + let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE); + Assert.equal(req.status, Cr.NS_ERROR_MALFORMED_URI); + await httpserv.stop(); +}); diff --git a/netwerk/test/unit/test_bug203271.js b/netwerk/test/unit/test_bug203271.js new file mode 100644 index 0000000000..ea260bc0b4 --- /dev/null +++ b/netwerk/test/unit/test_bug203271.js @@ -0,0 +1,247 @@ +// +// Tests if a response with an Expires-header in the past +// and Cache-Control: max-age in the future works as +// specified in RFC 2616 section 14.9.3 by letting max-age +// take precedence + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + // original problem described in bug#203271 + { + url: "/precedence", + server: "0", + expected: "0", + responseheader: [ + "Expires: " + getDateString(-1), + "Cache-Control: max-age=3600", + ], + }, + + { + url: "/precedence?0", + server: "0", + expected: "0", + responseheader: [ + "Cache-Control: max-age=3600", + "Expires: " + getDateString(-1), + ], + }, + + // max-age=1s, expires=1 year from now + { + url: "/precedence?1", + server: "0", + expected: "0", + responseheader: [ + "Expires: " + getDateString(1), + "Cache-Control: max-age=1", + ], + }, + + // expires=now + { + url: "/precedence?2", + server: "0", + expected: "0", + responseheader: ["Expires: " + getDateString(0)], + }, + + // max-age=1s + { + url: "/precedence?3", + server: "0", + expected: "0", + responseheader: ["Cache-Control: max-age=1"], + }, + + // The test below is the example from + // + // https://bugzilla.mozilla.org/show_bug.cgi?id=203271#c27 + // + // max-age=2592000s (1 month), expires=1 year from now, date=1 year ago + { + url: "/precedence?4", + server: "0", + expected: "0", + responseheader: [ + "Cache-Control: private, max-age=2592000", + "Expires: " + getDateString(+1), + ], + explicitDate: getDateString(-1), + }, + + // The two tests below are also examples of clocks really out of synch + // max-age=1s, date=1 year from now + { + url: "/precedence?5", + server: "0", + expected: "0", + responseheader: ["Cache-Control: max-age=1"], + explicitDate: getDateString(1), + }, + + // max-age=60s, date=1 year from now + { + url: "/precedence?6", + server: "0", + expected: "0", + responseheader: ["Cache-Control: max-age=60"], + explicitDate: getDateString(1), + }, + + // this is just to get a pause of 3s to allow cache-entries to expire + { url: "/precedence?999", server: "0", expected: "0", delay: "3000" }, + + // Below are the cases which actually matters + { url: "/precedence", server: "1", expected: "0" }, // should be cached + + { url: "/precedence?0", server: "1", expected: "0" }, // should be cached + + { url: "/precedence?1", server: "1", expected: "1" }, // should have expired + + { url: "/precedence?2", server: "1", expected: "1" }, // should have expired + + { url: "/precedence?3", server: "1", expected: "1" }, // should have expired + + { url: "/precedence?4", server: "1", expected: "1" }, // should have expired + + { url: "/precedence?5", server: "1", expected: "1" }, // should have expired + + { url: "/precedence?6", server: "1", expected: "0" }, // should be cached +]; + +function logit(i, data, ctx) { + dump( + "requested [" + + tests[i].server + + "] " + + "got [" + + data + + "] " + + "expected [" + + tests[i].expected + + "]" + ); + + if (tests[i].responseheader) { + dump("\t[" + tests[i].responseheader + "]"); + } + dump("\n"); + // Dump all response-headers + dump("\n===================================\n"); + ctx.visitResponseHeaders({ + visitHeader(key, val) { + dump("\t" + key + ":" + val + "\n"); + }, + }); + dump("===================================\n"); +} + +function setupChannel(suffix, value) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.requestMethod = "GET"; // default value, just being paranoid... + httpChan.setRequestHeader("x-request", value, false); + return httpChan; +} + +function triggerNextTest() { + var channel = setupChannel(tests[index].url, tests[index].server); + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, channel)); +} + +function checkValueAndTrigger(request, data, ctx) { + logit(index, data, ctx); + Assert.equal(tests[index].expected, data); + + if (index < tests.length - 1) { + var delay = tests[index++].delay; + if (delay) { + do_timeout(delay, triggerNextTest); + } else { + triggerNextTest(); + } + } else { + httpserver.stop(do_test_finished); + } +} + +function run_test() { + httpserver.registerPathHandler("/precedence", handler); + httpserver.start(-1); + + // clear cache + evict_cache_entries(); + + triggerNextTest(); + do_test_pending(); +} + +function handler(metadata, response) { + var body = metadata.getHeader("x-request"); + response.setHeader("Content-Type", "text/plain", false); + + var date = tests[index].explicitDate; + if (date == undefined) { + response.setHeader("Date", getDateString(0), false); + } else { + response.setHeader("Date", date, false); + } + + var header = tests[index].responseheader; + if (header == undefined) { + response.setHeader("Last-Modified", getDateString(-1), false); + } else { + for (var i = 0; i < header.length; i++) { + var splitHdr = header[i].split(": "); + response.setHeader(splitHdr[0], splitHdr[1], false); + } + } + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + +function getDateString(yearDelta) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + (d.getUTCFullYear() + yearDelta) + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_bug248970_cache.js b/netwerk/test/unit/test_bug248970_cache.js new file mode 100644 index 0000000000..d52d3b88d9 --- /dev/null +++ b/netwerk/test/unit/test_bug248970_cache.js @@ -0,0 +1,144 @@ +/* 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"; + +// names for cache devices +const kDiskDevice = "disk"; +const kMemoryDevice = "memory"; + +const kCacheA = "http://cache/A"; +const kCacheA2 = "http://cache/A2"; +const kCacheB = "http://cache/B"; +const kTestContent = "test content"; + +const entries = [ + // key content device should exist after leaving PB + [kCacheA, kTestContent, kMemoryDevice, true], + [kCacheA2, kTestContent, kDiskDevice, false], + [kCacheB, kTestContent, kDiskDevice, true], +]; + +var store_idx; +var store_cb = null; + +function store_entries(cb) { + if (cb) { + store_cb = cb; + store_idx = 0; + } + + if (store_idx == entries.length) { + executeSoon(store_cb); + return; + } + + asyncOpenCacheEntry( + entries[store_idx][0], + entries[store_idx][2], + Ci.nsICacheStorage.OPEN_TRUNCATE, + Services.loadContextInfo.custom(false, { + privateBrowsingId: entries[store_idx][3] ? 0 : 1, + }), + store_data + ); +} + +var store_data = function (status, entry) { + Assert.equal(status, Cr.NS_OK); + var os = entry.openOutputStream(0, entries[store_idx][1].length); + + var written = os.write(entries[store_idx][1], entries[store_idx][1].length); + if (written != entries[store_idx][1].length) { + do_throw( + "os.write has not written all data!\n" + + " Expected: " + + entries[store_idx][1].length + + "\n" + + " Actual: " + + written + + "\n" + ); + } + os.close(); + entry.close(); + store_idx++; + executeSoon(store_entries); +}; + +var check_idx; +var check_cb = null; +var check_pb_exited; +function check_entries(cb, pbExited) { + if (cb) { + check_cb = cb; + check_idx = 0; + check_pb_exited = pbExited; + } + + if (check_idx == entries.length) { + executeSoon(check_cb); + return; + } + + asyncOpenCacheEntry( + entries[check_idx][0], + entries[check_idx][2], + Ci.nsICacheStorage.OPEN_READONLY, + Services.loadContextInfo.custom(false, { + privateBrowsingId: entries[check_idx][3] ? 0 : 1, + }), + check_data + ); +} + +var check_data = function (status, entry) { + var cont = function () { + check_idx++; + executeSoon(check_entries); + }; + + if (!check_pb_exited || entries[check_idx][3]) { + Assert.equal(status, Cr.NS_OK); + var is = entry.openInputStream(0); + pumpReadStream(is, function (read) { + entry.close(); + Assert.equal(read, entries[check_idx][1]); + cont(); + }); + } else { + Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + cont(); + } +}; + +function run_test() { + // Simulate a profile dir for xpcshell + do_get_profile(); + + // Start off with an empty cache + evict_cache_entries(); + + // Store cache-A, cache-A2, cache-B and cache-C + store_entries(run_test2); + + do_test_pending(); +} + +function run_test2() { + // Check if cache-A, cache-A2, cache-B and cache-C are available + check_entries(run_test3, false); +} + +function run_test3() { + // Simulate all private browsing instances being closed + Services.obs.notifyObservers(null, "last-pb-context-exited"); + + // Make sure the memory device is not empty + get_device_entry_count(kMemoryDevice, null, function (count) { + Assert.equal(count, 1); + // Check if cache-A is gone, and cache-B and cache-C are still available + check_entries(do_test_finished, true); + }); +} diff --git a/netwerk/test/unit/test_bug248970_cookie.js b/netwerk/test/unit/test_bug248970_cookie.js new file mode 100644 index 0000000000..0cdbb769c1 --- /dev/null +++ b/netwerk/test/unit/test_bug248970_cookie.js @@ -0,0 +1,146 @@ +/* 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 { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver; + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} +function makeChan(path) { + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + "/" + path, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function setup_chan(path, isPrivate, callback) { + var chan = makeChan(path); + chan.QueryInterface(Ci.nsIPrivateBrowsingChannel).setPrivate(isPrivate); + chan.asyncOpen(new ChannelListener(callback)); +} + +function set_cookie(value, callback) { + return setup_chan("set?cookie=" + value, false, callback); +} + +function set_private_cookie(value, callback) { + return setup_chan("set?cookie=" + value, true, callback); +} + +function check_cookie_presence(value, isPrivate, expected, callback) { + setup_chan( + "present?cookie=" + value.replace("=", "|"), + isPrivate, + function (req) { + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.responseStatus, expected ? 200 : 404); + callback(req); + } + ); +} + +function presentHandler(metadata, response) { + var present = false; + var match = /cookie=([^&]*)/.exec(metadata.queryString); + if (match) { + try { + present = metadata + .getHeader("Cookie") + .includes(match[1].replace("|", "=")); + } catch (x) {} + } + response.setStatusLine("1.0", present ? 200 : 404, ""); +} + +function setHandler(metadata, response) { + response.setStatusLine("1.0", 200, "Cookie set"); + var match = /cookie=([^&]*)/.exec(metadata.queryString); + if (match) { + response.setHeader("Set-Cookie", match[1]); + } +} + +function run_test() { + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + httpserver = new HttpServer(); + httpserver.registerPathHandler("/set", setHandler); + httpserver.registerPathHandler("/present", presentHandler); + httpserver.start(-1); + + do_test_pending(); + + function check_cookie(req) { + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.responseStatus, 200); + try { + Assert.ok( + req.getResponseHeader("Set-Cookie") != "", + "expected a Set-Cookie header" + ); + } catch (x) { + do_throw("missing Set-Cookie header"); + } + + runNextTest(); + } + + let tests = []; + + function runNextTest() { + executeSoon(tests.shift()); + } + + tests.push(function () { + set_cookie("C1=V1", check_cookie); + }); + tests.push(function () { + set_private_cookie("C2=V2", check_cookie); + }); + tests.push(function () { + // Check that the first cookie is present in a non-private request + check_cookie_presence("C1=V1", false, true, runNextTest); + }); + tests.push(function () { + // Check that the second cookie is present in a private request + check_cookie_presence("C2=V2", true, true, runNextTest); + }); + tests.push(function () { + // Check that the first cookie is not present in a private request + check_cookie_presence("C1=V1", true, false, runNextTest); + }); + tests.push(function () { + // Check that the second cookie is not present in a non-private request + check_cookie_presence("C2=V2", false, false, runNextTest); + }); + + // The following test only works in a non-e10s situation at the moment, + // since the notification needs to run in the parent process but there is + // no existing mechanism to make that happen. + if (!inChildProcess()) { + tests.push(function () { + // Simulate all private browsing instances being closed + Services.obs.notifyObservers(null, "last-pb-context-exited"); + // Check that all private cookies are now unavailable in new private requests + check_cookie_presence("C2=V2", true, false, runNextTest); + }); + } + + tests.push(function () { + httpserver.stop(do_test_finished); + }); + + runNextTest(); +} diff --git a/netwerk/test/unit/test_bug261425.js b/netwerk/test/unit/test_bug261425.js new file mode 100644 index 0000000000..f58ad62e93 --- /dev/null +++ b/netwerk/test/unit/test_bug261425.js @@ -0,0 +1,29 @@ +"use strict"; + +function run_test() { + var newURI = Services.io.newURI("http://foo.com"); + + var success = false; + try { + newURI = newURI.mutate().setSpec("http: //foo.com").finalize(); + } catch (e) { + success = e.result == Cr.NS_ERROR_MALFORMED_URI; + } + if (!success) { + do_throw( + "We didn't throw NS_ERROR_MALFORMED_URI when a space was passed in the hostname!" + ); + } + + success = false; + try { + newURI.mutate().setHost(" foo.com").finalize(); + } catch (e) { + success = e.result == Cr.NS_ERROR_MALFORMED_URI; + } + if (!success) { + do_throw( + "We didn't throw NS_ERROR_MALFORMED_URI when a space was passed in the hostname!" + ); + } +} diff --git a/netwerk/test/unit/test_bug263127.js b/netwerk/test/unit/test_bug263127.js new file mode 100644 index 0000000000..e6718fb3a8 --- /dev/null +++ b/netwerk/test/unit/test_bug263127.js @@ -0,0 +1,56 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var server; +const BUGID = "263127"; + +var listener = { + QueryInterface: ChromeUtils.generateQI(["nsIDownloadObserver"]), + + onDownloadComplete(downloader, request, status, file) { + do_test_pending(); + server.stop(do_test_finished); + + if (!file) { + do_throw("Download failed"); + } + + try { + file.remove(false); + } catch (e) { + do_throw(e); + } + + Assert.ok(!file.exists()); + + do_test_finished(); + }, +}; + +function run_test() { + // start server + server = new HttpServer(); + server.start(-1); + + // Initialize downloader + var channel = NetUtil.newChannel({ + uri: "http://localhost:" + server.identity.primaryPort + "/", + loadUsingSystemPrincipal: true, + }); + var targetFile = Services.dirsvc.get("TmpD", Ci.nsIFile); + targetFile.append("bug" + BUGID + ".test"); + if (targetFile.exists()) { + targetFile.remove(false); + } + + var downloader = Cc["@mozilla.org/network/downloader;1"].createInstance( + Ci.nsIDownloader + ); + downloader.init(listener, targetFile); + + // Start download + channel.asyncOpen(downloader); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug282432.js b/netwerk/test/unit/test_bug282432.js new file mode 100644 index 0000000000..5e6e7a19b6 --- /dev/null +++ b/netwerk/test/unit/test_bug282432.js @@ -0,0 +1,37 @@ +"use strict"; + +function run_test() { + do_test_pending(); + + function StreamListener() {} + + StreamListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(aRequest) {}, + + onStopRequest(aRequest, aStatusCode) { + // Make sure we can catch the error NS_ERROR_FILE_NOT_FOUND here. + Assert.equal(aStatusCode, Cr.NS_ERROR_FILE_NOT_FOUND); + do_test_finished(); + }, + + onDataAvailable(aRequest, aStream, aOffset, aCount) { + do_throw("The channel must not call onDataAvailable()."); + }, + }; + + let listener = new StreamListener(); + + // This file does not exist. + let file = do_get_file("_NOT_EXIST_.txt", true); + Assert.ok(!file.exists()); + let channel = NetUtil.newChannel({ + uri: Services.io.newFileURI(file), + loadUsingSystemPrincipal: true, + }); + channel.asyncOpen(listener); +} diff --git a/netwerk/test/unit/test_bug321706.js b/netwerk/test/unit/test_bug321706.js new file mode 100644 index 0000000000..a267d6586d --- /dev/null +++ b/netwerk/test/unit/test_bug321706.js @@ -0,0 +1,10 @@ +"use strict"; + +const url = "http://foo.com/folder/file?/."; + +function run_test() { + var newURI = Services.io.newURI(url); + Assert.equal(newURI.spec, url); + Assert.equal(newURI.pathQueryRef, "/folder/file?/."); + Assert.equal(newURI.resolve("./file?/."), url); +} diff --git a/netwerk/test/unit/test_bug331825.js b/netwerk/test/unit/test_bug331825.js new file mode 100644 index 0000000000..d421ca5975 --- /dev/null +++ b/netwerk/test/unit/test_bug331825.js @@ -0,0 +1,41 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var server; +const BUGID = "331825"; + +function TestListener() {} +TestListener.prototype.onStartRequest = function (request) {}; +TestListener.prototype.onStopRequest = function (request, status) { + var channel = request.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(channel.responseStatus, 304); + + server.stop(do_test_finished); +}; + +function run_test() { + // start server + server = new HttpServer(); + + server.registerPathHandler("/bug" + BUGID, bug331825); + + server.start(-1); + + // make request + var channel = NetUtil.newChannel({ + uri: "http://localhost:" + server.identity.primaryPort + "/bug" + BUGID, + loadUsingSystemPrincipal: true, + }); + + channel.QueryInterface(Ci.nsIHttpChannel); + channel.setRequestHeader("If-None-Match", "foobar", false); + channel.asyncOpen(new TestListener()); + + do_test_pending(); +} + +// PATH HANDLER FOR /bug331825 +function bug331825(metadata, response) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); +} diff --git a/netwerk/test/unit/test_bug336501.js b/netwerk/test/unit/test_bug336501.js new file mode 100644 index 0000000000..3e15fd42bb --- /dev/null +++ b/netwerk/test/unit/test_bug336501.js @@ -0,0 +1,26 @@ +"use strict"; + +function run_test() { + var f = do_get_file("test_bug336501.js"); + + var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fis.init(f, -1, -1, 0); + + var bis = Cc["@mozilla.org/network/buffered-input-stream;1"].createInstance( + Ci.nsIBufferedInputStream + ); + bis.init(fis, 32); + + var sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sis.init(bis); + + sis.read(45); + sis.close(); + + var data = sis.read(45); + Assert.equal(data.length, 0); +} diff --git a/netwerk/test/unit/test_bug337744.js b/netwerk/test/unit/test_bug337744.js new file mode 100644 index 0000000000..69a99b8765 --- /dev/null +++ b/netwerk/test/unit/test_bug337744.js @@ -0,0 +1,126 @@ +/* verify that certain invalid URIs are not parsed by the resource + protocol handler */ + +"use strict"; + +const specs = [ + "resource://res-test//", + "resource://res-test/?foo=http:", + "resource://res-test/?foo=" + encodeURIComponent("http://example.com/"), + "resource://res-test/?foo=" + encodeURIComponent("x\\y"), + "resource://res-test/..%2F", + "resource://res-test/..%2f", + "resource://res-test/..%2F..", + "resource://res-test/..%2f..", + "resource://res-test/../../", + "resource://res-test/http://www.mozilla.org/", + "resource://res-test/file:///", +]; + +const error_specs = [ + "resource://res-test/..\\", + "resource://res-test/..\\..\\", + "resource://res-test/..%5C", + "resource://res-test/..%5c", +]; + +// Create some fake principal that has not enough +// privileges to access any resource: uri. +var uri = NetUtil.newURI("http://www.example.com"); +var principal = Services.scriptSecurityManager.createContentPrincipal(uri, {}); + +function get_channel(spec) { + var channel = NetUtil.newChannel({ + uri: NetUtil.newURI(spec), + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + Assert.throws( + () => { + channel.asyncOpen(null); + }, + /NS_ERROR_DOM_BAD_URI/, + `asyncOpen() of uri: ${spec} should throw` + ); + Assert.throws( + () => { + channel.open(); + }, + /NS_ERROR_DOM_BAD_URI/, + `Open() of uri: ${spec} should throw` + ); + + return channel; +} + +function check_safe_resolution(spec, rootURI) { + info(`Testing URL "${spec}"`); + + let channel = get_channel(spec); + + ok( + channel.name.startsWith(rootURI), + `URL resolved safely to ${channel.name}` + ); + let startOfQuery = channel.name.indexOf("?"); + if (startOfQuery == -1) { + ok(!/%2f/i.test(channel.name), `URL contains no escaped / characters`); + } else { + // Escaped slashes are allowed in the query or hash part of the URL + ok( + !channel.name.replace(/\?.*/, "").includes("%2f"), + `URL contains no escaped slashes before the query ${channel.name}` + ); + } +} + +function check_resolution_error(spec) { + Assert.throws( + () => { + get_channel(spec); + }, + /NS_ERROR_MALFORMED_URI/, + "Expected a malformed URI error" + ); +} + +function run_test() { + // resource:/// and resource://gre/ are resolved specially, so we need + // to create a temporary resource package to test the standard logic + // with. + + let resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService( + Ci.nsIResProtocolHandler + ); + let rootFile = Services.dirsvc.get("GreD", Ci.nsIFile); + let rootURI = Services.io.newFileURI(rootFile); + + rootFile.append("directory-that-does-not-exist"); + let inexistentURI = Services.io.newFileURI(rootFile); + + resProto.setSubstitution("res-test", rootURI); + resProto.setSubstitution("res-inexistent", inexistentURI); + registerCleanupFunction(() => { + resProto.setSubstitution("res-test", null); + resProto.setSubstitution("res-inexistent", null); + }); + + let baseRoot = resProto.resolveURI(Services.io.newURI("resource:///")); + let greRoot = resProto.resolveURI(Services.io.newURI("resource://gre/")); + + for (let spec of specs) { + check_safe_resolution(spec, rootURI.spec); + check_safe_resolution( + spec.replace("res-test", "res-inexistent"), + inexistentURI.spec + ); + check_safe_resolution(spec.replace("res-test", ""), baseRoot); + check_safe_resolution(spec.replace("res-test", "gre"), greRoot); + } + + for (let spec of error_specs) { + check_resolution_error(spec); + } +} diff --git a/netwerk/test/unit/test_bug368702.js b/netwerk/test/unit/test_bug368702.js new file mode 100644 index 0000000000..c77f40a71b --- /dev/null +++ b/netwerk/test/unit/test_bug368702.js @@ -0,0 +1,147 @@ +"use strict"; + +function run_test() { + var tld = Services.eTLD; + Assert.equal(tld.getPublicSuffixFromHost("localhost"), "localhost"); + Assert.equal(tld.getPublicSuffixFromHost("localhost."), "localhost."); + Assert.equal(tld.getPublicSuffixFromHost("domain.com"), "com"); + Assert.equal(tld.getPublicSuffixFromHost("domain.com."), "com."); + Assert.equal(tld.getPublicSuffixFromHost("domain.co.uk"), "co.uk"); + Assert.equal(tld.getPublicSuffixFromHost("domain.co.uk."), "co.uk."); + Assert.equal(tld.getPublicSuffixFromHost("co.uk"), "co.uk"); + Assert.equal(tld.getBaseDomainFromHost("domain.co.uk"), "domain.co.uk"); + Assert.equal(tld.getBaseDomainFromHost("domain.co.uk."), "domain.co.uk."); + + try { + tld.getPublicSuffixFromHost(""); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); + } + + try { + tld.getBaseDomainFromHost("domain.co.uk", 1); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); + } + + try { + tld.getBaseDomainFromHost("co.uk"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); + } + + try { + tld.getBaseDomainFromHost(""); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); + } + + try { + tld.getPublicSuffixFromHost("1.2.3.4"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + tld.getPublicSuffixFromHost("2010:836B:4179::836B:4179"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + tld.getPublicSuffixFromHost("3232235878"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + tld.getPublicSuffixFromHost("::ffff:192.9.5.5"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + tld.getPublicSuffixFromHost("::1"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + // Check IP addresses with trailing dot as well, Necko sometimes accepts + // those (depending on operating system, see bug 380543) + try { + tld.getPublicSuffixFromHost("127.0.0.1."); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + tld.getPublicSuffixFromHost("::ffff:127.0.0.1."); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + // check normalization: output should be consistent with + // nsIURI::GetAsciiHost(), i.e. lowercased and ASCII/ACE encoded + var uri = Services.io.newURI("http://b\u00FCcher.co.uk"); + Assert.equal(tld.getBaseDomain(uri), "xn--bcher-kva.co.uk"); + Assert.equal( + tld.getBaseDomainFromHost("b\u00FCcher.co.uk"), + "xn--bcher-kva.co.uk" + ); + Assert.equal(tld.getPublicSuffix(uri), "co.uk"); + Assert.equal(tld.getPublicSuffixFromHost("b\u00FCcher.co.uk"), "co.uk"); + + // check that malformed hosts are rejected as invalid args + try { + tld.getBaseDomainFromHost("domain.co.uk.."); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + tld.getBaseDomainFromHost("domain.co..uk"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + tld.getBaseDomainFromHost(".domain.co.uk"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + tld.getBaseDomainFromHost(".domain.co.uk"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + tld.getBaseDomainFromHost("."); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + tld.getBaseDomainFromHost(".."); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } +} diff --git a/netwerk/test/unit/test_bug369787.js b/netwerk/test/unit/test_bug369787.js new file mode 100644 index 0000000000..b8f54a7215 --- /dev/null +++ b/netwerk/test/unit/test_bug369787.js @@ -0,0 +1,71 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const BUGID = "369787"; +var server = null; +var channel = null; + +function change_content_type() { + var origType = channel.contentType; + const newType = "x-foo/x-bar"; + channel.contentType = newType; + Assert.equal(channel.contentType, newType); + channel.contentType = origType; + Assert.equal(channel.contentType, origType); +} + +function TestListener() {} +TestListener.prototype.onStartRequest = function (request) { + try { + // request might be different from channel + channel = request.QueryInterface(Ci.nsIChannel); + + change_content_type(); + } catch (ex) { + print(ex); + throw ex; + } +}; +TestListener.prototype.onStopRequest = function (request, status) { + try { + change_content_type(); + } catch (ex) { + print(ex); + // don't re-throw ex to avoid hanging the test + } + + do_timeout(0, after_channel_closed); +}; + +function after_channel_closed() { + try { + change_content_type(); + } finally { + server.stop(do_test_finished); + } +} + +function run_test() { + // start server + server = new HttpServer(); + + server.registerPathHandler("/bug" + BUGID, bug369787); + + server.start(-1); + + // make request + channel = NetUtil.newChannel({ + uri: "http://localhost:" + server.identity.primaryPort + "/bug" + BUGID, + loadUsingSystemPrincipal: true, + }); + channel.QueryInterface(Ci.nsIHttpChannel); + channel.asyncOpen(new TestListener()); + + do_test_pending(); +} + +// PATH HANDLER FOR /bug369787 +function bug369787(metadata, response) { + /* do nothing */ +} diff --git a/netwerk/test/unit/test_bug371473.js b/netwerk/test/unit/test_bug371473.js new file mode 100644 index 0000000000..999a6c00cf --- /dev/null +++ b/netwerk/test/unit/test_bug371473.js @@ -0,0 +1,36 @@ +"use strict"; + +function test_not_too_long() { + var spec = "jar:http://example.com/bar.jar!/"; + try { + Services.io.newURI(spec); + } catch (e) { + do_throw("newURI threw even though it wasn't passed a large nested URI?"); + } +} + +function test_too_long() { + var i; + var prefix = "jar:"; + for (i = 0; i < 16; i++) { + prefix = prefix + prefix; + } + var suffix = "!/"; + for (i = 0; i < 16; i++) { + suffix = suffix + suffix; + } + + var spec = prefix + "http://example.com/bar.jar" + suffix; + try { + // The following will produce a recursive call that if + // unchecked would lead to a stack overflow. If we + // do not crash here and thus an exception is caught + // we have passed the test. + Services.io.newURI(spec); + } catch (e) {} +} + +function run_test() { + test_not_too_long(); + test_too_long(); +} diff --git a/netwerk/test/unit/test_bug376844.js b/netwerk/test/unit/test_bug376844.js new file mode 100644 index 0000000000..5184a30e54 --- /dev/null +++ b/netwerk/test/unit/test_bug376844.js @@ -0,0 +1,19 @@ +"use strict"; + +const testURLs = [ + ["http://example.com/<", "http://example.com/%3C"], + ["http://example.com/>", "http://example.com/%3E"], + ["http://example.com/'", "http://example.com/'"], + ['http://example.com/"', "http://example.com/%22"], + ["http://example.com/?<", "http://example.com/?%3C"], + ["http://example.com/?>", "http://example.com/?%3E"], + ["http://example.com/?'", "http://example.com/?%27"], + ['http://example.com/?"', "http://example.com/?%22"], +]; + +function run_test() { + for (var i = 0; i < testURLs.length; i++) { + var uri = Services.io.newURI(testURLs[i][0]); + Assert.equal(uri.spec, testURLs[i][1]); + } +} diff --git a/netwerk/test/unit/test_bug376865.js b/netwerk/test/unit/test_bug376865.js new file mode 100644 index 0000000000..260dcbf7e6 --- /dev/null +++ b/netwerk/test/unit/test_bug376865.js @@ -0,0 +1,23 @@ +"use strict"; + +function run_test() { + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsISupportsCString + ); + stream.data = "foo bar baz"; + + var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance( + Ci.nsIInputStreamPump + ); + pump.init(stream, 0, 0, false); + + // When we pass a null listener argument too asyncRead we expect it to throw + // instead of crashing. + try { + pump.asyncRead(null); + } catch (e) { + return; + } + + do_throw("asyncRead didn't throw when passed a null listener argument."); +} diff --git a/netwerk/test/unit/test_bug379034.js b/netwerk/test/unit/test_bug379034.js new file mode 100644 index 0000000000..eb28daf51e --- /dev/null +++ b/netwerk/test/unit/test_bug379034.js @@ -0,0 +1,19 @@ +"use strict"; + +function run_test() { + const ios = Services.io; + + var base = ios.newURI("http://localhost/bug379034/index.html"); + + var uri = ios.newURI("http:a.html", null, base); + Assert.equal(uri.spec, "http://localhost/bug379034/a.html"); + + uri = ios.newURI("HtTp:b.html", null, base); + Assert.equal(uri.spec, "http://localhost/bug379034/b.html"); + + uri = ios.newURI("https:c.html", null, base); + Assert.equal(uri.spec, "https://c.html/"); + + uri = ios.newURI("./https:d.html", null, base); + Assert.equal(uri.spec, "http://localhost/bug379034/https:d.html"); +} diff --git a/netwerk/test/unit/test_bug380994.js b/netwerk/test/unit/test_bug380994.js new file mode 100644 index 0000000000..c46d9b29ed --- /dev/null +++ b/netwerk/test/unit/test_bug380994.js @@ -0,0 +1,24 @@ +/* check resource: protocol for traversal problems */ + +"use strict"; + +const specs = [ + "resource:///chrome/../plugins", + "resource:///chrome%2f../plugins", + "resource:///chrome/..%2fplugins", + "resource:///chrome%2f%2e%2e%2fplugins", + "resource:///../../../..", + "resource:///..%2f..%2f..%2f..", + "resource:///%2e%2e", +]; + +function run_test() { + for (var spec of specs) { + var uri = Services.io.newURI(spec); + if (uri.spec.includes("..")) { + do_throw( + "resource: traversal remains: '" + spec + "' ==> '" + uri.spec + "'" + ); + } + } +} diff --git a/netwerk/test/unit/test_bug388281.js b/netwerk/test/unit/test_bug388281.js new file mode 100644 index 0000000000..5ded2ad2b5 --- /dev/null +++ b/netwerk/test/unit/test_bug388281.js @@ -0,0 +1,25 @@ +"use strict"; + +function run_test() { + const ios = Services.io; + + var uri = ios.newURI("http://foo.com/file.txt"); + uri = uri.mutate().setPort(90).finalize(); + Assert.equal(uri.hostPort, "foo.com:90"); + + uri = ios.newURI("http://foo.com:10/file.txt"); + uri = uri.mutate().setPort(500).finalize(); + Assert.equal(uri.hostPort, "foo.com:500"); + + uri = ios.newURI("http://foo.com:5000/file.txt"); + uri = uri.mutate().setPort(20).finalize(); + Assert.equal(uri.hostPort, "foo.com:20"); + + uri = ios.newURI("http://foo.com:5000/file.txt"); + uri = uri.mutate().setPort(-1).finalize(); + Assert.equal(uri.hostPort, "foo.com"); + + uri = ios.newURI("http://foo.com:5000/file.txt"); + uri = uri.mutate().setPort(80).finalize(); + Assert.equal(uri.hostPort, "foo.com"); +} diff --git a/netwerk/test/unit/test_bug396389.js b/netwerk/test/unit/test_bug396389.js new file mode 100644 index 0000000000..cb71f549a6 --- /dev/null +++ b/netwerk/test/unit/test_bug396389.js @@ -0,0 +1,63 @@ +"use strict"; + +function round_trip(uri) { + var objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIObjectOutputStream + ); + var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(false, false, 0, 0xffffffff, null); + objectOutStream.setOutputStream(pipe.outputStream); + objectOutStream.writeCompoundObject(uri, Ci.nsISupports, true); + objectOutStream.close(); + + var objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIObjectInputStream + ); + objectInStream.setInputStream(pipe.inputStream); + return objectInStream.readObject(true).QueryInterface(Ci.nsIURI); +} + +var prefData = [ + { + name: "network.IDN_show_punycode", + newVal: false, + }, +]; + +function run_test() { + var uri1 = Services.io.newURI("file:///"); + Assert.ok(uri1 instanceof Ci.nsIFileURL); + + var uri2 = uri1.mutate().finalize(); + Assert.ok(uri2 instanceof Ci.nsIFileURL); + Assert.ok(uri1.equals(uri2)); + + var uri3 = round_trip(uri1); + Assert.ok(uri3 instanceof Ci.nsIFileURL); + Assert.ok(uri1.equals(uri3)); + + // Make sure our prefs are set such that this test actually means something + var prefs = Services.prefs; + for (let pref of prefData) { + prefs.setBoolPref(pref.name, pref.newVal); + } + + try { + // URI stolen from + // http://lists.w3.org/Archives/Public/public-iri/2004Mar/0012.html + var uri4 = Services.io.newURI("http://xn--jos-dma.example.net.ch/"); + Assert.equal(uri4.asciiHost, "xn--jos-dma.example.net.ch"); + Assert.equal(uri4.displayHost, "jos\u00e9.example.net.ch"); + + var uri5 = round_trip(uri4); + Assert.ok(uri4.equals(uri5)); + Assert.equal(uri4.displayHost, uri5.displayHost); + Assert.equal(uri4.asciiHost, uri5.asciiHost); + } finally { + for (let pref of prefData) { + if (prefs.prefHasUserValue(pref.name)) { + prefs.clearUserPref(pref.name); + } + } + } +} diff --git a/netwerk/test/unit/test_bug401564.js b/netwerk/test/unit/test_bug401564.js new file mode 100644 index 0000000000..9de47eb1b4 --- /dev/null +++ b/netwerk/test/unit/test_bug401564.js @@ -0,0 +1,40 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; +const noRedirectURI = "/content"; +const acceptType = "application/json"; + +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Moved Temporarily"); + response.setHeader("Location", noRedirectURI, false); +} + +function contentHandler(metadata, response) { + Assert.equal(metadata.getHeader("Accept"), acceptType); + httpserver.stop(do_test_finished); +} + +function dummyHandler(request, buffer) {} + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/redirect", redirectHandler); + httpserver.registerPathHandler("/content", contentHandler); + httpserver.start(-1); + + Services.prefs.setBoolPref("network.http.prompt-temp-redirect", false); + + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + "/redirect", + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.setRequestHeader("Accept", acceptType, false); + + chan.asyncOpen(new ChannelListener(dummyHandler, null)); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug411952.js b/netwerk/test/unit/test_bug411952.js new file mode 100644 index 0000000000..4584957188 --- /dev/null +++ b/netwerk/test/unit/test_bug411952.js @@ -0,0 +1,53 @@ +"use strict"; + +function run_test() { + try { + var cm = Services.cookies; + Assert.notEqual(cm, null, "Retrieving the cookie manager failed"); + + const time = new Date("Jan 1, 2030").getTime() / 1000; + cm.add( + "example.com", + "/", + "C", + "V", + false, + true, + false, + time, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + const now = Math.floor(new Date().getTime() / 1000); + + var found = false; + for (let cookie of cm.cookies) { + if ( + cookie.host == "example.com" && + cookie.path == "/" && + cookie.name == "C" + ) { + Assert.ok( + "creationTime" in cookie, + "creationTime attribute is not accessible on the cookie" + ); + var creationTime = Math.floor(cookie.creationTime / 1000000); + // allow the times to slip by one second at most, + // which should be fine under normal circumstances. + Assert.ok( + Math.abs(creationTime - now) <= 1, + "Cookie's creationTime is set incorrectly" + ); + found = true; + break; + } + } + + Assert.ok(found, "Didn't find the cookie we were after"); + } catch (e) { + do_throw("Unexpected exception: " + e.toString()); + } + + do_test_finished(); +} diff --git a/netwerk/test/unit/test_bug412457.js b/netwerk/test/unit/test_bug412457.js new file mode 100644 index 0000000000..30fd2ed3fc --- /dev/null +++ b/netwerk/test/unit/test_bug412457.js @@ -0,0 +1,79 @@ +"use strict"; + +function run_test() { + // check if hostname is unescaped before applying IDNA + var newURI = Services.io.newURI("http://\u5341%2ecom/"); + Assert.equal(newURI.asciiHost, "xn--kkr.com"); + + // escaped UTF8 + newURI = newURI.mutate().setSpec("http://%e5%8d%81.com").finalize(); + Assert.equal(newURI.asciiHost, "xn--kkr.com"); + + // There should be only allowed characters in hostname after + // unescaping and attempting to apply IDNA. "\x80" is illegal in + // UTF-8, so IDNA fails, and 0x80 is illegal in DNS too. + Assert.throws( + () => { + newURI = newURI.mutate().setSpec("http://%80.com").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "illegal UTF character" + ); + + // test parsing URL with all possible host terminators + newURI = newURI.mutate().setSpec("http://example.com?foo").finalize(); + Assert.equal(newURI.asciiHost, "example.com"); + + newURI = newURI.mutate().setSpec("http://example.com#foo").finalize(); + Assert.equal(newURI.asciiHost, "example.com"); + + newURI = newURI.mutate().setSpec("http://example.com:80").finalize(); + Assert.equal(newURI.asciiHost, "example.com"); + + newURI = newURI.mutate().setSpec("http://example.com/foo").finalize(); + Assert.equal(newURI.asciiHost, "example.com"); + + // Characters that are invalid in the host + Assert.throws( + () => { + newURI = newURI.mutate().setSpec("http://example.com%3ffoo").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); + Assert.throws( + () => { + newURI = newURI.mutate().setSpec("http://example.com%23foo").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); + Assert.throws( + () => { + newURI = newURI.mutate().setSpec("http://example.com%3bfoo").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); + Assert.throws( + () => { + newURI = newURI.mutate().setSpec("http://example.com%3a80").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); + Assert.throws( + () => { + newURI = newURI.mutate().setSpec("http://example.com%2ffoo").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); + Assert.throws( + () => { + newURI = newURI.mutate().setSpec("http://example.com%00").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); +} diff --git a/netwerk/test/unit/test_bug412945.js b/netwerk/test/unit/test_bug412945.js new file mode 100644 index 0000000000..81a959eb6a --- /dev/null +++ b/netwerk/test/unit/test_bug412945.js @@ -0,0 +1,42 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserv; + +function TestListener() {} + +TestListener.prototype.onStartRequest = function (request) {}; + +TestListener.prototype.onStopRequest = function (request, status) { + httpserv.stop(do_test_finished); +}; + +function run_test() { + httpserv = new HttpServer(); + + httpserv.registerPathHandler("/bug412945", bug412945); + + httpserv.start(-1); + + // make request + var channel = NetUtil.newChannel({ + uri: "http://localhost:" + httpserv.identity.primaryPort + "/bug412945", + loadUsingSystemPrincipal: true, + }); + + channel.QueryInterface(Ci.nsIHttpChannel); + channel.requestMethod = "POST"; + channel.asyncOpen(new TestListener(), null); + + do_test_pending(); +} + +function bug412945(metadata, response) { + if ( + !metadata.hasHeader("Content-Length") || + metadata.getHeader("Content-Length") != "0" + ) { + do_throw("Content-Length header not found!"); + } +} diff --git a/netwerk/test/unit/test_bug414122.js b/netwerk/test/unit/test_bug414122.js new file mode 100644 index 0000000000..66c2bdf4bd --- /dev/null +++ b/netwerk/test/unit/test_bug414122.js @@ -0,0 +1,59 @@ +"use strict"; + +const PR_RDONLY = 0x1; + +var idn = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService); + +function run_test() { + var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fis.init( + do_get_file("effective_tld_names.dat"), + PR_RDONLY, + 0o444, + Ci.nsIFileInputStream.CLOSE_ON_EOF + ); + + var lis = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance( + Ci.nsIConverterInputStream + ); + lis.init(fis, "UTF-8", 1024, 0); + lis.QueryInterface(Ci.nsIUnicharLineInputStream); + + var out = { value: "" }; + do { + var more = lis.readLine(out); + var line = out.value; + + line = line.replace(/^\s+/, ""); + var firstTwo = line.substring(0, 2); // a misnomer, but whatever + if (firstTwo == "" || firstTwo == "//") { + continue; + } + + var space = line.search(/[ \t]/); + line = line.substring(0, space == -1 ? line.length : space); + + if ("*." == firstTwo) { + let rest = line.substring(2); + checkPublicSuffix( + "foo.SUPER-SPECIAL-AWESOME-PREFIX." + rest, + "SUPER-SPECIAL-AWESOME-PREFIX." + rest + ); + } else if ("!" == line.charAt(0)) { + checkPublicSuffix( + line.substring(1), + line.substring(line.indexOf(".") + 1) + ); + } else { + checkPublicSuffix("SUPER-SPECIAL-AWESOME-PREFIX." + line, line); + } + } while (more); +} + +function checkPublicSuffix(host, expectedSuffix) { + expectedSuffix = idn.convertUTF8toACE(expectedSuffix).toLowerCase(); + var actualSuffix = Services.eTLD.getPublicSuffixFromHost(host); + Assert.equal(actualSuffix, expectedSuffix); +} diff --git a/netwerk/test/unit/test_bug427957.js b/netwerk/test/unit/test_bug427957.js new file mode 100644 index 0000000000..33a2444c9d --- /dev/null +++ b/netwerk/test/unit/test_bug427957.js @@ -0,0 +1,100 @@ +/** + * Test for Bidi restrictions on IDNs from RFC 3454 + */ + +"use strict"; + +var idnService; + +function expected_pass(inputIDN) { + var isASCII = {}; + var displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII); + Assert.equal(displayIDN, inputIDN); +} + +function expected_fail(inputIDN) { + var isASCII = {}; + var displayIDN = ""; + + try { + displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII); + } catch (e) {} + + Assert.notEqual(displayIDN, inputIDN); +} + +function run_test() { + idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + /* + * In any profile that specifies bidirectional character handling, all + * three of the following requirements MUST be met: + * + * 1) The characters in section 5.8 MUST be prohibited. + */ + + // 0340; COMBINING GRAVE TONE MARK + expected_fail("foo\u0340bar.com"); + // 0341; COMBINING ACUTE TONE MARK + expected_fail("foo\u0341bar.com"); + // 200E; LEFT-TO-RIGHT MARK + expected_fail("foo\u200ebar.com"); + // 200F; RIGHT-TO-LEFT MARK + // Note: this is an RTL IDN so that it doesn't fail test 2) below + expected_fail( + "\u200f\u0645\u062B\u0627\u0644.\u0622\u0632\u0645\u0627\u06CC\u0634\u06CC" + ); + // 202A; LEFT-TO-RIGHT EMBEDDING + expected_fail("foo\u202abar.com"); + // 202B; RIGHT-TO-LEFT EMBEDDING + expected_fail("foo\u202bbar.com"); + // 202C; POP DIRECTIONAL FORMATTING + expected_fail("foo\u202cbar.com"); + // 202D; LEFT-TO-RIGHT OVERRIDE + expected_fail("foo\u202dbar.com"); + // 202E; RIGHT-TO-LEFT OVERRIDE + expected_fail("foo\u202ebar.com"); + // 206A; INHIBIT SYMMETRIC SWAPPING + expected_fail("foo\u206abar.com"); + // 206B; ACTIVATE SYMMETRIC SWAPPING + expected_fail("foo\u206bbar.com"); + // 206C; INHIBIT ARABIC FORM SHAPING + expected_fail("foo\u206cbar.com"); + // 206D; ACTIVATE ARABIC FORM SHAPING + expected_fail("foo\u206dbar.com"); + // 206E; NATIONAL DIGIT SHAPES + expected_fail("foo\u206ebar.com"); + // 206F; NOMINAL DIGIT SHAPES + expected_fail("foo\u206fbar.com"); + + /* + * 2) If a string contains any RandALCat character, the string MUST NOT + * contain any LCat character. + */ + + // www.מיץpetel.com is invalid + expected_fail("www.\u05DE\u05D9\u05E5petel.com"); + // But www.מיץפטל.com is fine because the ltr and rtl characters are in + // different labels + expected_pass("www.\u05DE\u05D9\u05E5\u05E4\u05D8\u05DC.com"); + + /* + * 3) If a string contains any RandALCat character, a RandALCat + * character MUST be the first character of the string, and a + * RandALCat character MUST be the last character of the string. + */ + + // www.1מיץ.com is invalid + expected_fail("www.1\u05DE\u05D9\u05E5.com"); + // www.!מיץ.com is invalid + expected_fail("www.!\u05DE\u05D9\u05E5.com"); + // www.מיץ!.com is invalid + expected_fail("www.\u05DE\u05D9\u05E5!.com"); + + // XXX TODO: add a test for an RTL label ending with a digit. This was + // invalid in IDNA2003 but became valid in IDNA2008 + + // But www.מיץ1פטל.com is fine + expected_pass("www.\u05DE\u05D9\u05E51\u05E4\u05D8\u05DC.com"); +} diff --git a/netwerk/test/unit/test_bug429347.js b/netwerk/test/unit/test_bug429347.js new file mode 100644 index 0000000000..ad6c508eb6 --- /dev/null +++ b/netwerk/test/unit/test_bug429347.js @@ -0,0 +1,39 @@ +"use strict"; + +function run_test() { + var ios = Services.io; + + var uri1 = ios.newURI("http://example.com#bar"); + var uri2 = ios.newURI("http://example.com/#bar"); + Assert.ok(uri1.equals(uri2)); + + uri1 = uri1.mutate().setSpec("http://example.com?bar").finalize(); + uri2 = uri2.mutate().setSpec("http://example.com/?bar").finalize(); + Assert.ok(uri1.equals(uri2)); + + // see https://bugzilla.mozilla.org/show_bug.cgi?id=665706 + // ";" is not parsed as special anymore and thus ends up + // in the authority component (see RFC 3986) + uri1 = uri1.mutate().setSpec("http://example.com;bar").finalize(); + uri2 = uri2.mutate().setSpec("http://example.com/;bar").finalize(); + Assert.ok(!uri1.equals(uri2)); + + uri1 = uri1.mutate().setSpec("http://example.com#").finalize(); + uri2 = uri2.mutate().setSpec("http://example.com/#").finalize(); + Assert.ok(uri1.equals(uri2)); + + uri1 = uri1.mutate().setSpec("http://example.com?").finalize(); + uri2 = uri2.mutate().setSpec("http://example.com/?").finalize(); + Assert.ok(uri1.equals(uri2)); + + // see https://bugzilla.mozilla.org/show_bug.cgi?id=665706 + // ";" is not parsed as special anymore and thus ends up + // in the authority component (see RFC 3986) + uri1 = uri1.mutate().setSpec("http://example.com;").finalize(); + uri2 = uri2.mutate().setSpec("http://example.com/;").finalize(); + Assert.ok(!uri1.equals(uri2)); + + uri1 = uri1.mutate().setSpec("http://example.com").finalize(); + uri2 = uri2.mutate().setSpec("http://example.com/").finalize(); + Assert.ok(uri1.equals(uri2)); +} diff --git a/netwerk/test/unit/test_bug455311.js b/netwerk/test/unit/test_bug455311.js new file mode 100644 index 0000000000..36e000a174 --- /dev/null +++ b/netwerk/test/unit/test_bug455311.js @@ -0,0 +1,128 @@ +"use strict"; + +function getUrlLinkFile() { + if (mozinfo.os == "win") { + return do_get_file("test_link.url"); + } + if (mozinfo.os == "linux") { + return do_get_file("test_link.desktop"); + } + do_throw("Unexpected platform"); + return null; +} + +const ios = Services.io; + +function NotificationCallbacks(origURI, newURI) { + this._origURI = origURI; + this._newURI = newURI; +} +NotificationCallbacks.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIChannelEventSink", + ]), + getInterface(iid) { + return this.QueryInterface(iid); + }, + asyncOnChannelRedirect(oldChan, newChan, flags, callback) { + Assert.equal(oldChan.URI.spec, this._origURI.spec); + Assert.equal(oldChan.URI, this._origURI); + Assert.equal(oldChan.originalURI.spec, this._origURI.spec); + Assert.equal(oldChan.originalURI, this._origURI); + Assert.equal(newChan.originalURI.spec, this._newURI.spec); + Assert.equal(newChan.originalURI, newChan.URI); + Assert.equal(newChan.URI.spec, this._newURI.spec); + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, +}; + +function RequestObserver(origURI, newURI, nextTest) { + this._origURI = origURI; + this._newURI = newURI; + this._nextTest = nextTest; +} +RequestObserver.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIRequestObserver", + "nsIStreamListener", + ]), + onStartRequest(req) { + var chan = req.QueryInterface(Ci.nsIChannel); + Assert.equal(chan.URI.spec, this._origURI.spec); + Assert.equal(chan.URI, this._origURI); + Assert.equal(chan.originalURI.spec, this._origURI.spec); + Assert.equal(chan.originalURI, this._origURI); + }, + onDataAvailable(req, stream, offset, count) { + do_throw("Unexpected call to onDataAvailable"); + }, + onStopRequest(req, status) { + var chan = req.QueryInterface(Ci.nsIChannel); + try { + Assert.equal(chan.URI.spec, this._origURI.spec); + Assert.equal(chan.URI, this._origURI); + Assert.equal(chan.originalURI.spec, this._origURI.spec); + Assert.equal(chan.originalURI, this._origURI); + Assert.equal(status, Cr.NS_ERROR_ABORT); + Assert.ok(!chan.isPending()); + } catch (e) {} + this._nextTest(); + }, +}; + +function test_cancel(linkURI, newURI) { + var chan = NetUtil.newChannel({ + uri: linkURI, + loadUsingSystemPrincipal: true, + }); + Assert.equal(chan.URI, linkURI); + Assert.equal(chan.originalURI, linkURI); + chan.asyncOpen(new RequestObserver(linkURI, newURI, do_test_finished)); + Assert.ok(chan.isPending()); + chan.cancel(Cr.NS_ERROR_ABORT); + Assert.ok(chan.isPending()); +} + +function test_channel(linkURI, newURI) { + const chan = NetUtil.newChannel({ + uri: linkURI, + loadUsingSystemPrincipal: true, + }); + Assert.equal(chan.URI, linkURI); + Assert.equal(chan.originalURI, linkURI); + chan.notificationCallbacks = new NotificationCallbacks(linkURI, newURI); + chan.asyncOpen( + new RequestObserver(linkURI, newURI, () => test_cancel(linkURI, newURI)) + ); + Assert.ok(chan.isPending()); +} + +function run_test() { + if (mozinfo.os != "win" && mozinfo.os != "linux") { + return; + } + + let link = getUrlLinkFile(); + let linkURI; + if (link.isSymlink()) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(link.target); + linkURI = ios.newFileURI(file); + } else { + linkURI = ios.newFileURI(link); + } + + do_test_pending(); + test_channel(linkURI, ios.newURI("http://www.mozilla.org/")); + + if (mozinfo.os != "win") { + return; + } + + link = do_get_file("test_link.lnk"); + test_channel( + ios.newFileURI(link), + ios.newURI("file:///Z:/moz-nonexistent/index.html") + ); +} diff --git a/netwerk/test/unit/test_bug464591.js b/netwerk/test/unit/test_bug464591.js new file mode 100644 index 0000000000..bc2f481a0e --- /dev/null +++ b/netwerk/test/unit/test_bug464591.js @@ -0,0 +1,94 @@ +// 1.percent-encoded IDN that contains blacklisted character should be converted +// to punycode, not UTF-8 string +// 2.only hostname-valid percent encoded ASCII characters should be decoded +// 3.IDN convertion must not bypassed by %00 + +"use strict"; + +let reference = [ + [ + "www.example.com%e2%88%95www.mozill%d0%b0.com%e2%81%84www.mozilla.org", + "www.example.xn--comwww-re3c.xn--mozill-8nf.xn--comwww-rq0c.mozilla.org", + ], +]; + +let badURIs = [ + ["www.mozill%61%2f.org"], // a slash is not valid in the hostname + ["www.e%00xample.com%e2%88%95www.mozill%d0%b0.com%e2%81%84www.mozill%61.org"], +]; + +let prefData = [ + { + name: "network.enableIDN", + newVal: true, + }, + { + name: "network.IDN_show_punycode", + newVal: false, + }, +]; + +let prefIdnBlackList = { + name: "network.IDN.extra_blocked_chars", + minimumList: "\u2215\u0430\u2044", +}; + +function stringToURL(str) { + return Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIStandardURLMutator) + .init(Ci.nsIStandardURL.URLTYPE_AUTHORITY, 80, str, "UTF-8", null) + .finalize() + .QueryInterface(Ci.nsIURL); +} + +function run_test() { + // Make sure our prefs are set such that this test actually means something + let prefs = Services.prefs; + for (let pref of prefData) { + prefs.setBoolPref(pref.name, pref.newVal); + } + + prefIdnBlackList.set = false; + try { + prefIdnBlackList.oldVal = prefs.getComplexValue( + prefIdnBlackList.name, + Ci.nsIPrefLocalizedString + ).data; + prefs.getComplexValue( + prefIdnBlackList.name, + Ci.nsIPrefLocalizedString + ).data = prefIdnBlackList.minimumList; + prefIdnBlackList.set = true; + } catch (e) {} + + registerCleanupFunction(function () { + for (let pref of prefData) { + prefs.clearUserPref(pref.name); + } + if (prefIdnBlackList.set) { + prefs.getComplexValue( + prefIdnBlackList.name, + Ci.nsIPrefLocalizedString + ).data = prefIdnBlackList.oldVal; + } + }); + + for (let i = 0; i < reference.length; ++i) { + try { + let result = stringToURL("http://" + reference[i][0]).host; + equal(result, reference[i][1]); + } catch (e) { + ok(false, "Error testing " + reference[i][0]); + } + } + + for (let i = 0; i < badURIs.length; ++i) { + Assert.throws( + () => { + stringToURL("http://" + badURIs[i][0]).host; + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); + } +} diff --git a/netwerk/test/unit/test_bug468426.js b/netwerk/test/unit/test_bug468426.js new file mode 100644 index 0000000000..f8f6b8ed62 --- /dev/null +++ b/netwerk/test/unit/test_bug468426.js @@ -0,0 +1,129 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + // Initial request. Cached variant will have no cookie + { url: "/bug468426", server: "0", expected: "0", cookie: null }, + + // Cache now contains a variant with no value for cookie. If we don't + // set cookie we expect to receive the cached variant + { url: "/bug468426", server: "1", expected: "0", cookie: null }, + + // Cache still contains a variant with no value for cookie. If we + // set a value for cookie we expect a fresh value + { url: "/bug468426", server: "2", expected: "2", cookie: "c=2" }, + + // Cache now contains a variant with cookie "c=2". If the request + // also set cookie "c=2", we expect to receive the cached variant. + { url: "/bug468426", server: "3", expected: "2", cookie: "c=2" }, + + // Cache still contains a variant with cookie "c=2". When setting + // cookie "c=4" in the request we expect a fresh value + { url: "/bug468426", server: "4", expected: "4", cookie: "c=4" }, + + // Cache now contains a variant with cookie "c=4". When setting + // cookie "c=4" in the request we expect the cached variant + { url: "/bug468426", server: "5", expected: "4", cookie: "c=4" }, + + // Cache still contains a variant with cookie "c=4". When setting + // no cookie in the request we expect a fresh value + { url: "/bug468426", server: "6", expected: "6", cookie: null }, +]; + +function setupChannel(suffix, value, cookie) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.requestMethod = "GET"; + httpChan.setRequestHeader("x-request", value, false); + if (cookie != null) { + httpChan.setRequestHeader("Cookie", cookie, false); + } + return httpChan; +} + +function triggerNextTest() { + var channel = setupChannel( + tests[index].url, + tests[index].server, + tests[index].cookie + ); + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null)); +} + +function checkValueAndTrigger(request, data, ctx) { + Assert.equal(tests[index].expected, data); + + if (index < tests.length - 1) { + index++; + // This call happens in onStopRequest from the channel. Opening a new + // channel to the same url here is no good idea! Post it instead... + do_timeout(1, triggerNextTest); + } else { + httpserver.stop(do_test_finished); + } +} + +function run_test() { + httpserver.registerPathHandler("/bug468426", handler); + httpserver.start(-1); + + // Clear cache and trigger the first test + evict_cache_entries(); + triggerNextTest(); + + do_test_pending(); +} + +function handler(metadata, response) { + var body = "unset"; + try { + body = metadata.getHeader("x-request"); + } catch (e) {} + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Last-Modified", getDateString(-1), false); + response.setHeader("Vary", "Cookie", false); + response.bodyOutputStream.write(body, body.length); +} + +function getDateString(yearDelta) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + (d.getUTCFullYear() + yearDelta) + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_bug468594.js b/netwerk/test/unit/test_bug468594.js new file mode 100644 index 0000000000..545ae75e5f --- /dev/null +++ b/netwerk/test/unit/test_bug468594.js @@ -0,0 +1,176 @@ +// +// This script emulates the test called "Freshness" +// by Mark Nottingham, located at +// +// http://mnot.net/javascript/xmlhttprequest/cache.html +// +// The issue with Mr. Nottinghams page is that the server +// always seems to send an Expires-header in the response, +// breaking the finer details of the test. This script has +// full control of response-headers, however, and can perform +// the intended testing plus some extra stuff. +// +// Please see RFC 2616 section 13.2.1 6th paragraph for the +// definition of "explicit expiration time" being used here. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + { url: "/freshness", server: "0", expected: "0" }, + { url: "/freshness", server: "1", expected: "0" }, // cached + + // RFC 2616 section 13.9 2nd paragraph says not to heuristically cache + // querystring, but we allow it to maintain web compat + { url: "/freshness?a", server: "2", expected: "2" }, + { url: "/freshness?a", server: "3", expected: "2" }, + + // explicit expiration dates in the future should be cached + { + url: "/freshness?b", + server: "4", + expected: "4", + responseheader: "Expires: " + getDateString(1), + }, + { url: "/freshness?b", server: "5", expected: "4" }, // cached due to Expires + + { + url: "/freshness?c", + server: "6", + expected: "6", + responseheader: "Cache-Control: max-age=3600", + }, + { url: "/freshness?c", server: "7", expected: "6" }, // cached due to max-age + + // explicit expiration dates in the past should NOT be cached + { + url: "/freshness?d", + server: "8", + expected: "8", + responseheader: "Expires: " + getDateString(-1), + }, + { url: "/freshness?d", server: "9", expected: "9" }, + + { + url: "/freshness?e", + server: "10", + expected: "10", + responseheader: "Cache-Control: max-age=0", + }, + { url: "/freshness?e", server: "11", expected: "11" }, + + { url: "/freshness", server: "99", expected: "0" }, // cached +]; + +function logit(i, data) { + dump( + tests[i].url + + "\t requested [" + + tests[i].server + + "]" + + " got [" + + data + + "] expected [" + + tests[i].expected + + "]" + ); + if (tests[i].responseheader) { + dump("\t[" + tests[i].responseheader + "]"); + } + dump("\n"); +} + +function setupChannel(suffix, value) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.requestMethod = "GET"; + httpChan.setRequestHeader("x-request", value, false); + return httpChan; +} + +function triggerNextTest() { + var channel = setupChannel(tests[index].url, tests[index].server); + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null)); +} + +function checkValueAndTrigger(request, data, ctx) { + logit(index, data); + Assert.equal(tests[index].expected, data); + + if (index < tests.length - 1) { + index++; + triggerNextTest(); + } else { + httpserver.stop(do_test_finished); + } +} + +function run_test() { + httpserver.registerPathHandler("/freshness", handler); + httpserver.start(-1); + + // clear cache + evict_cache_entries(); + triggerNextTest(); + + do_test_pending(); +} + +function handler(metadata, response) { + var body = metadata.getHeader("x-request"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Date", getDateString(0), false); + + var header = tests[index].responseheader; + if (header == null) { + response.setHeader("Last-Modified", getDateString(-1), false); + } else { + var splitHdr = header.split(": "); + response.setHeader(splitHdr[0], splitHdr[1], false); + } + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + +function getDateString(yearDelta) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + (d.getUTCFullYear() + yearDelta) + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_bug470716.js b/netwerk/test/unit/test_bug470716.js new file mode 100644 index 0000000000..009e7eee11 --- /dev/null +++ b/netwerk/test/unit/test_bug470716.js @@ -0,0 +1,171 @@ +"use strict"; + +var CC = Components.Constructor; + +const StreamCopier = CC( + "@mozilla.org/network/async-stream-copier;1", + "nsIAsyncStreamCopier", + "init" +); + +const ScriptableInputStream = CC( + "@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", + "init" +); + +const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init"); + +var pipe1; +var pipe2; +var copier; +var test_result; +var test_content; +var test_source_closed; +var test_sink_closed; +var test_nr; + +var copyObserver = { + onStartRequest(request) {}, + + onStopRequest(request, statusCode) { + // check status code + Assert.equal(statusCode, test_result); + + // check number of copied bytes + Assert.equal(pipe2.inputStream.available(), test_content.length); + + // check content + var scinp = new ScriptableInputStream(pipe2.inputStream); + var content = scinp.read(scinp.available()); + Assert.equal(content, test_content); + + // check closed sink + try { + pipe2.outputStream.write("closedSinkTest", 14); + Assert.ok(!test_sink_closed); + } catch (ex) { + Assert.ok(test_sink_closed); + } + + // check closed source + try { + pipe1.outputStream.write("closedSourceTest", 16); + Assert.ok(!test_source_closed); + } catch (ex) { + Assert.ok(test_source_closed); + } + + do_timeout(0, do_test); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]), +}; + +function startCopier(closeSource, closeSink) { + pipe1 = new Pipe( + true /* nonBlockingInput */, + true /* nonBlockingOutput */, + 0 /* segmentSize */, + 0xffffffff /* segmentCount */, + null /* segmentAllocator */ + ); + + pipe2 = new Pipe( + true /* nonBlockingInput */, + true /* nonBlockingOutput */, + 0 /* segmentSize */, + 0xffffffff /* segmentCount */, + null /* segmentAllocator */ + ); + + copier = new StreamCopier( + pipe1.inputStream /* aSource */, + pipe2.outputStream /* aSink */, + null /* aTarget */, + true /* aSourceBuffered */, + true /* aSinkBuffered */, + 8192 /* aChunkSize */, + closeSource /* aCloseSource */, + closeSink /* aCloseSink */ + ); + + copier.asyncCopy(copyObserver, null); +} + +function do_test() { + test_nr++; + test_content = "test" + test_nr; + + switch (test_nr) { + case 1: + case 2: // close sink + case 3: // close source + case 4: // close both + // test canceling transfer + // use some undefined error code to check if it is successfully passed + // to the request observer + test_result = 0x87654321; + + test_source_closed = (test_nr - 1) >> 1 != 0; + test_sink_closed = (test_nr - 1) % 2 != 0; + + startCopier(test_source_closed, test_sink_closed); + pipe1.outputStream.write(test_content, test_content.length); + pipe1.outputStream.flush(); + do_timeout(20, function () { + copier.cancel(test_result); + pipe1.outputStream.write("a", 1); + }); + break; + case 5: + case 6: // close sink + case 7: // close source + case 8: // close both + // test copying with EOF on source + test_result = 0; + + test_source_closed = (test_nr - 5) >> 1 != 0; + test_sink_closed = (test_nr - 5) % 2 != 0; + + startCopier(test_source_closed, test_sink_closed); + pipe1.outputStream.write(test_content, test_content.length); + // we will close the source + test_source_closed = true; + pipe1.outputStream.close(); + break; + case 9: + case 10: // close sink + case 11: // close source + case 12: // close both + // test copying with error on sink + // use some undefined error code to check if it is successfully passed + // to the request observer + test_result = 0x87654321; + + test_source_closed = (test_nr - 9) >> 1 != 0; + test_sink_closed = (test_nr - 9) % 2 != 0; + + startCopier(test_source_closed, test_sink_closed); + pipe1.outputStream.write(test_content, test_content.length); + pipe1.outputStream.flush(); + // we will close the sink + test_sink_closed = true; + do_timeout(20, function () { + pipe2.outputStream + .QueryInterface(Ci.nsIAsyncOutputStream) + .closeWithStatus(test_result); + pipe1.outputStream.write("a", 1); + }); + break; + case 13: + do_test_finished(); + break; + } +} + +function run_test() { + test_nr = 0; + do_timeout(0, do_test); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug477578.js b/netwerk/test/unit/test_bug477578.js new file mode 100644 index 0000000000..c21b1281da --- /dev/null +++ b/netwerk/test/unit/test_bug477578.js @@ -0,0 +1,51 @@ +// test that methods are not normalized + +"use strict"; + +const testMethods = [ + ["GET"], + ["get"], + ["Get"], + ["gET"], + ["gEt"], + ["post"], + ["POST"], + ["head"], + ["HEAD"], + ["put"], + ["PUT"], + ["delete"], + ["DELETE"], + ["connect"], + ["CONNECT"], + ["options"], + ["trace"], + ["track"], + ["copy"], + ["index"], + ["lock"], + ["m-post"], + ["mkcol"], + ["move"], + ["propfind"], + ["proppatch"], + ["unlock"], + ["link"], + ["LINK"], + ["foo"], + ["foO"], + ["fOo"], + ["Foo"], +]; + +function run_test() { + var chan = NetUtil.newChannel({ + uri: "http://localhost/", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + + for (var i = 0; i < testMethods.length; i++) { + chan.requestMethod = testMethods[i]; + Assert.equal(chan.requestMethod, testMethods[i]); + } +} diff --git a/netwerk/test/unit/test_bug479413.js b/netwerk/test/unit/test_bug479413.js new file mode 100644 index 0000000000..c28f3da412 --- /dev/null +++ b/netwerk/test/unit/test_bug479413.js @@ -0,0 +1,48 @@ +/** + * Test for unassigned code points in IDNs (RFC 3454 section 7) + */ + +"use strict"; + +var idnService; + +function expected_pass(inputIDN) { + var isASCII = {}; + var displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII); + Assert.equal(displayIDN, inputIDN); +} + +function expected_fail(inputIDN) { + var isASCII = {}; + var displayIDN = ""; + + try { + displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII); + } catch (e) {} + + Assert.notEqual(displayIDN, inputIDN); +} + +function run_test() { + idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + + // assigned code point + expected_pass("foo\u0101bar.com"); + + // assigned code point in punycode. Should *fail* because the URL will be + // converted to Unicode for display + expected_fail("xn--foobar-5za.com"); + + // unassigned code point + expected_fail("foo\u3040bar.com"); + + // unassigned code point in punycode. Should *pass* because the URL will not + // be converted to Unicode + expected_pass("xn--foobar-533e.com"); + + // code point assigned since Unicode 3.0 + // XXX This test will unexpectedly pass when we update to IDNAbis + expected_fail("foo\u0370bar.com"); +} diff --git a/netwerk/test/unit/test_bug479485.js b/netwerk/test/unit/test_bug479485.js new file mode 100644 index 0000000000..30e2e0428d --- /dev/null +++ b/netwerk/test/unit/test_bug479485.js @@ -0,0 +1,65 @@ +"use strict"; + +function run_test() { + var ios = Services.io; + + var test_port = function (port, exception_expected) { + dump((port || "no port provided") + "\n"); + var exception_threw = false; + try { + var newURI = ios.newURI("http://foo.com" + port); + } catch (e) { + exception_threw = e.result == Cr.NS_ERROR_MALFORMED_URI; + } + if (exception_threw != exception_expected) { + do_throw( + "We did" + + (exception_expected ? "n't" : "") + + " throw NS_ERROR_MALFORMED_URI when creating a new URI with " + + port + + " as a port" + ); + } + Assert.equal(exception_threw, exception_expected); + + exception_threw = false; + newURI = ios.newURI("http://foo.com"); + try { + newURI + .mutate() + .setSpec("http://foo.com" + port) + .finalize(); + } catch (e) { + exception_threw = e.result == Cr.NS_ERROR_MALFORMED_URI; + } + if (exception_threw != exception_expected) { + do_throw( + "We did" + + (exception_expected ? "n't" : "") + + " throw NS_ERROR_MALFORMED_URI when setting a spec of a URI with " + + port + + " as a port" + ); + } + Assert.equal(exception_threw, exception_expected); + }; + + test_port(":invalid", true); + test_port(":-2", true); + test_port(":-1", true); + test_port(":0", false); + test_port( + ":185891548721348172817857824356013651809236172635716571865023757816234081723451516780356", + true + ); + + // Following 3 tests are all failing, we do not throw, although we parse the whole string and use only 5870 as a portnumber + test_port(":5870:80", true); + test_port(":5870-80", true); + test_port(":5870+80", true); + + // Just a regression check + test_port(":5870", false); + test_port(":80", false); + test_port("", false); +} diff --git a/netwerk/test/unit/test_bug482601.js b/netwerk/test/unit/test_bug482601.js new file mode 100644 index 0000000000..a583a21663 --- /dev/null +++ b/netwerk/test/unit/test_bug482601.js @@ -0,0 +1,262 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserv = null; +var test_nr = 0; +var observers_called = ""; +var handlers_called = ""; +var buffer = ""; + +var observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(subject, topic, data) { + if (observers_called.length) { + observers_called += ","; + } + + observers_called += topic; + }, +}; + +var listener = { + onStartRequest(request) { + buffer = ""; + }, + + onDataAvailable(request, stream, offset, count) { + buffer = buffer.concat(read_stream(stream, count)); + }, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + Assert.equal(buffer, "0123456789"); + Assert.equal(observers_called, results[test_nr]); + test_nr++; + do_timeout(0, do_test); + }, +}; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/bug482601/nocache", bug482601_nocache); + httpserv.registerPathHandler("/bug482601/partial", bug482601_partial); + httpserv.registerPathHandler("/bug482601/cached", bug482601_cached); + httpserv.registerPathHandler( + "/bug482601/only_from_cache", + bug482601_only_from_cache + ); + httpserv.start(-1); + + var obs = Cc["@mozilla.org/observer-service;1"].getService(); + obs = obs.QueryInterface(Ci.nsIObserverService); + obs.addObserver(observer, "http-on-examine-response"); + obs.addObserver(observer, "http-on-examine-merged-response"); + obs.addObserver(observer, "http-on-examine-cached-response"); + + do_timeout(0, do_test); + do_test_pending(); +} + +function do_test() { + if (test_nr < tests.length) { + tests[test_nr](); + } else { + Assert.equal(handlers_called, "nocache,partial,cached"); + httpserv.stop(do_test_finished); + } +} + +var tests = [test_nocache, test_partial, test_cached, test_only_from_cache]; + +var results = [ + "http-on-examine-response", + "http-on-examine-response,http-on-examine-merged-response", + "http-on-examine-response,http-on-examine-merged-response", + "http-on-examine-cached-response", +]; + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function storeCache(aCacheEntry, aResponseHeads, aContent) { + aCacheEntry.setMetaDataElement("request-method", "GET"); + aCacheEntry.setMetaDataElement("response-head", aResponseHeads); + aCacheEntry.setMetaDataElement("charset", "ISO-8859-1"); + + var oStream = aCacheEntry.openOutputStream(0, aContent.length); + var written = oStream.write(aContent, aContent.length); + if (written != aContent.length) { + do_throw( + "oStream.write has not written all data!\n" + + " Expected: " + + written + + "\n" + + " Actual: " + + aContent.length + + "\n" + ); + } + oStream.close(); + aCacheEntry.close(); +} + +function test_nocache() { + observers_called = ""; + + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/nocache" + ); + chan.asyncOpen(listener); +} + +function test_partial() { + asyncOpenCacheEntry( + "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/partial", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + test_partial2 + ); +} + +function test_partial2(status, entry) { + Assert.equal(status, Cr.NS_OK); + storeCache( + entry, + "HTTP/1.1 200 OK\r\n" + + "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Server: httpd.js\r\n" + + "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Accept-Ranges: bytes\r\n" + + "Content-Length: 10\r\n" + + "Content-Type: text/plain\r\n", + "0123" + ); + + observers_called = ""; + + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/partial" + ); + chan.asyncOpen(listener); +} + +function test_cached() { + asyncOpenCacheEntry( + "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/cached", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + test_cached2 + ); +} + +function test_cached2(status, entry) { + Assert.equal(status, Cr.NS_OK); + storeCache( + entry, + "HTTP/1.1 200 OK\r\n" + + "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Server: httpd.js\r\n" + + "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Accept-Ranges: bytes\r\n" + + "Content-Length: 10\r\n" + + "Content-Type: text/plain\r\n", + "0123456789" + ); + + observers_called = ""; + + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/cached" + ); + chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS; + chan.asyncOpen(listener); +} + +function test_only_from_cache() { + asyncOpenCacheEntry( + "http://localhost:" + + httpserv.identity.primaryPort + + "/bug482601/only_from_cache", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + test_only_from_cache2 + ); +} + +function test_only_from_cache2(status, entry) { + Assert.equal(status, Cr.NS_OK); + storeCache( + entry, + "HTTP/1.1 200 OK\r\n" + + "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Server: httpd.js\r\n" + + "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Accept-Ranges: bytes\r\n" + + "Content-Length: 10\r\n" + + "Content-Type: text/plain\r\n", + "0123456789" + ); + + observers_called = ""; + + var chan = makeChan( + "http://localhost:" + + httpserv.identity.primaryPort + + "/bug482601/only_from_cache" + ); + chan.loadFlags = Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE; + chan.asyncOpen(listener); +} + +// PATHS + +// /bug482601/nocache +function bug482601_nocache(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + var body = "0123456789"; + response.bodyOutputStream.write(body, body.length); + handlers_called += "nocache"; +} + +// /bug482601/partial +function bug482601_partial(metadata, response) { + Assert.ok(metadata.hasHeader("If-Range")); + Assert.equal(metadata.getHeader("If-Range"), "Thu, 1 Jan 2009 00:00:00 GMT"); + Assert.ok(metadata.hasHeader("Range")); + Assert.equal(metadata.getHeader("Range"), "bytes=4-"); + + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader("Content-Range", "bytes 4-9/10", false); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Last-Modified", "Thu, 1 Jan 2009 00:00:00 GMT"); + + var body = "456789"; + response.bodyOutputStream.write(body, body.length); + handlers_called += ",partial"; +} + +// /bug482601/cached +function bug482601_cached(metadata, response) { + Assert.ok(metadata.hasHeader("If-Modified-Since")); + Assert.equal( + metadata.getHeader("If-Modified-Since"), + "Thu, 1 Jan 2009 00:00:00 GMT" + ); + + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + handlers_called += ",cached"; +} + +// /bug482601/only_from_cache +function bug482601_only_from_cache(metadata, response) { + do_throw("This should not be reached"); +} diff --git a/netwerk/test/unit/test_bug482934.js b/netwerk/test/unit/test_bug482934.js new file mode 100644 index 0000000000..0c484be975 --- /dev/null +++ b/netwerk/test/unit/test_bug482934.js @@ -0,0 +1,188 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var response_code; +var response_body; + +var request_time; +var response_time; + +var cache_storage; + +var httpserver = new HttpServer(); +httpserver.start(-1); + +var base_url = "http://localhost:" + httpserver.identity.primaryPort; +var resource = "/resource"; +var resource_url = base_url + resource; + +// Test flags +var hit_server = false; + +function make_channel(aUrl) { + // Reset test global status + hit_server = false; + + var req = NetUtil.newChannel({ uri: aUrl, loadUsingSystemPrincipal: true }); + req.QueryInterface(Ci.nsIHttpChannel); + req.setRequestHeader("If-Modified-Since", request_time, false); + return req; +} + +function make_uri(aUrl) { + return Services.io.newURI(aUrl); +} + +function resource_handler(aMetadata, aResponse) { + hit_server = true; + Assert.ok(aMetadata.hasHeader("If-Modified-Since")); + Assert.equal(aMetadata.getHeader("If-Modified-Since"), request_time); + + if (response_code == "200") { + aResponse.setStatusLine(aMetadata.httpVersion, 200, "OK"); + aResponse.setHeader("Content-Type", "text/plain", false); + aResponse.setHeader("Last-Modified", response_time, false); + + aResponse.bodyOutputStream.write(response_body, response_body.length); + } else if (response_code == "304") { + aResponse.setStatusLine(aMetadata.httpVersion, 304, "Not Modified"); + aResponse.setHeader("Returned-From-Handler", "1"); + } +} + +function check_cached_data(aCachedData, aCallback) { + asyncOpenCacheEntry( + resource_url, + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + function (aStatus, aEntry) { + Assert.equal(aStatus, Cr.NS_OK); + pumpReadStream(aEntry.openInputStream(0), function (aData) { + Assert.equal(aData, aCachedData); + aCallback(); + }); + } + ); +} + +function run_test() { + do_get_profile(); + evict_cache_entries(); + + do_test_pending(); + + cache_storage = getCacheStorage("disk"); + httpserver.registerPathHandler(resource, resource_handler); + + wait_for_cache_index(run_next_test); +} + +// 1. send custom conditional request when we don't have an entry +// server returns 304 -> client receives 304 +add_test(() => { + response_code = "304"; + response_body = ""; + request_time = "Thu, 1 Jan 2009 00:00:00 GMT"; + response_time = "Thu, 1 Jan 2009 00:00:00 GMT"; + + var ch = make_channel(resource_url); + ch.asyncOpen( + new ChannelListener(function (aRequest, aData) { + syncWithCacheIOThread(() => { + Assert.ok(hit_server); + Assert.equal( + aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus, + 304 + ); + Assert.ok(!cache_storage.exists(make_uri(resource_url), "")); + Assert.equal(aRequest.getResponseHeader("Returned-From-Handler"), "1"); + + run_next_test(); + }, true); + }, null) + ); +}); + +// 2. send custom conditional request when we don't have an entry +// server returns 200 -> result is cached +add_test(() => { + response_code = "200"; + response_body = "content_body"; + request_time = "Thu, 1 Jan 2009 00:00:00 GMT"; + response_time = "Fri, 2 Jan 2009 00:00:00 GMT"; + + var ch = make_channel(resource_url); + ch.asyncOpen( + new ChannelListener(function (aRequest, aData) { + syncWithCacheIOThread(() => { + Assert.ok(hit_server); + Assert.equal( + aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus, + 200 + ); + Assert.ok(cache_storage.exists(make_uri(resource_url), "")); + + check_cached_data(response_body, run_next_test); + }, true); + }, null) + ); +}); + +// 3. send custom conditional request when we have an entry +// server returns 304 -> client receives 304 and cached entry is unchanged +add_test(() => { + response_code = "304"; + var cached_body = response_body; + response_body = ""; + request_time = "Fri, 2 Jan 2009 00:00:00 GMT"; + response_time = "Fri, 2 Jan 2009 00:00:00 GMT"; + + var ch = make_channel(resource_url); + ch.asyncOpen( + new ChannelListener(function (aRequest, aData) { + syncWithCacheIOThread(() => { + Assert.ok(hit_server); + Assert.equal( + aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus, + 304 + ); + Assert.ok(cache_storage.exists(make_uri(resource_url), "")); + Assert.equal(aRequest.getResponseHeader("Returned-From-Handler"), "1"); + Assert.equal(aData, ""); + + // Check the cache data is not changed + check_cached_data(cached_body, run_next_test); + }, true); + }, null) + ); +}); + +// 4. send custom conditional request when we have an entry +// server returns 200 -> result is cached +add_test(() => { + response_code = "200"; + response_body = "updated_content_body"; + request_time = "Fri, 2 Jan 2009 00:00:00 GMT"; + response_time = "Sat, 3 Jan 2009 00:00:00 GMT"; + var ch = make_channel(resource_url); + ch.asyncOpen( + new ChannelListener(function (aRequest, aData) { + syncWithCacheIOThread(() => { + Assert.ok(hit_server); + Assert.equal( + aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus, + 200 + ); + Assert.ok(cache_storage.exists(make_uri(resource_url), "")); + + // Check the cache data is updated + check_cached_data(response_body, () => { + run_next_test(); + httpserver.stop(do_test_finished); + }); + }, true); + }, null) + ); +}); diff --git a/netwerk/test/unit/test_bug490095.js b/netwerk/test/unit/test_bug490095.js new file mode 100644 index 0000000000..7e8de69af1 --- /dev/null +++ b/netwerk/test/unit/test_bug490095.js @@ -0,0 +1,158 @@ +// +// Verify that the VALIDATE_NEVER and LOAD_FROM_CACHE flags override +// heuristic query freshness as defined in RFC 2616 section 13.9 +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + { url: "/freshness?a", server: "0", expected: "0" }, + { url: "/freshness?a", server: "1", expected: "1" }, + + // Setting the VALIDATE_NEVER flag should grab entry from cache + { + url: "/freshness?a", + server: "2", + expected: "1", + flags: Ci.nsIRequest.VALIDATE_NEVER, + }, + + // Finally, check that request is validated with no flags set + { url: "/freshness?a", server: "99", expected: "99" }, + + { url: "/freshness?b", server: "0", expected: "0" }, + { url: "/freshness?b", server: "1", expected: "1" }, + + // Setting the LOAD_FROM_CACHE flag also grab the entry from cache + { + url: "/freshness?b", + server: "2", + expected: "1", + flags: Ci.nsIRequest.LOAD_FROM_CACHE, + }, + + // Finally, check that request is validated with no flags set + { url: "/freshness?b", server: "99", expected: "99" }, +]; + +function logit(i, data) { + dump( + tests[i].url + + "\t requested [" + + tests[i].server + + "]" + + " got [" + + data + + "] expected [" + + tests[i].expected + + "]" + ); + if (tests[i].responseheader) { + dump("\t[" + tests[i].responseheader + "]"); + } + dump("\n"); +} + +function setupChannel(suffix, value) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.requestMethod = "GET"; + httpChan.setRequestHeader("x-request", value, false); + return httpChan; +} + +function triggerNextTest() { + var test = tests[index]; + var channel = setupChannel(test.url, test.server); + if (test.flags) { + channel.loadFlags = test.flags; + } + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null)); +} + +function checkValueAndTrigger(request, data, ctx) { + logit(index, data); + Assert.equal(tests[index].expected, data); + + if (index < tests.length - 1) { + index++; + // this call happens in onStopRequest from the channel, and opening a + // new channel to the same url here is no good idea... post it instead + do_timeout(1, triggerNextTest); + } else { + httpserver.stop(do_test_finished); + } +} + +function run_test() { + httpserver.registerPathHandler("/freshness", handler); + httpserver.start(-1); + + // clear cache + evict_cache_entries(); + + triggerNextTest(); + + do_test_pending(); +} + +function handler(metadata, response) { + var body = metadata.getHeader("x-request"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Date", getDateString(0), false); + response.setHeader("Cache-Control", "max-age=0", false); + + var header = tests[index].responseheader; + if (header == null) { + response.setHeader("Last-Modified", getDateString(-1), false); + } else { + var splitHdr = header.split(": "); + response.setHeader(splitHdr[0], splitHdr[1], false); + } + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + +function getDateString(yearDelta) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + (d.getUTCFullYear() + yearDelta) + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_bug504014.js b/netwerk/test/unit/test_bug504014.js new file mode 100644 index 0000000000..f7e2ed2452 --- /dev/null +++ b/netwerk/test/unit/test_bug504014.js @@ -0,0 +1,72 @@ +"use strict"; + +var valid_URIs = [ + "http://[::]/", + "http://[::1]/", + "http://[1::]/", + "http://[::]/", + "http://[::1]/", + "http://[1::]/", + "http://[1:2:3:4:5:6:7::]/", + "http://[::1:2:3:4:5:6:7]/", + "http://[1:2:a:B:c:D:e:F]/", + "http://[1::8]/", + "http://[1:2::8]/", + "http://[0000:0123:4567:89AB:CDEF:abcd:ef00:0000]/", + "http://[::192.168.1.1]/", + "http://[1::0.0.0.0]/", + "http://[1:2::255.255.255.255]/", + "http://[1:2:3::255.255.255.255]/", + "http://[1:2:3:4::255.255.255.255]/", + "http://[1:2:3:4:5::255.255.255.255]/", + "http://[1:2:3:4:5:6:255.255.255.255]/", +]; + +var invalid_URIs = [ + "http://[1]/", + "http://[192.168.1.1]/", + "http://[:::]/", + "http://[:::1]/", + "http://[1:::]/", + "http://[::1::]/", + "http://[1:2:3:4:5:6:7:]/", + "http://[:2:3:4:5:6:7:8]/", + "http://[1:2:3:4:5:6:7:8:]/", + "http://[:1:2:3:4:5:6:7:8]/", + "http://[1:2:3:4:5:6:7:8::]/", + "http://[::1:2:3:4:5:6:7:8]/", + "http://[1:2:3:4:5:6:7]/", + "http://[1:2:3:4:5:6:7:8:9]/", + "http://[00001:2:3:4:5:6:7:8]/", + "http://[0001:2:3:4:5:6:7:89abc]/", + "http://[A:b:C:d:E:f:G:h]/", + "http://[::192.168.1]/", + "http://[::192.168.1.]/", + "http://[::.168.1.1]/", + "http://[::192..1.1]/", + "http://[::0192.168.1.1]/", + "http://[::256.255.255.255]/", + "http://[::1x.255.255.255]/", + "http://[::192.4294967464.1.1]/", + "http://[1:2:3:4:5:6::255.255.255.255]/", + "http://[1:2:3:4:5:6:7:255.255.255.255]/", +]; + +function run_test() { + for (let i = 0; i < valid_URIs.length; i++) { + try { + Services.io.newURI(valid_URIs[i]); + } catch (e) { + do_throw("cannot create URI:" + valid_URIs[i]); + } + } + + for (let i = 0; i < invalid_URIs.length; i++) { + try { + Services.io.newURI(invalid_URIs[i]); + do_throw("should throw: " + invalid_URIs[i]); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_MALFORMED_URI); + } + } +} diff --git a/netwerk/test/unit/test_bug510359.js b/netwerk/test/unit/test_bug510359.js new file mode 100644 index 0000000000..321da39912 --- /dev/null +++ b/netwerk/test/unit/test_bug510359.js @@ -0,0 +1,102 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + { url: "/bug510359", server: "0", expected: "0" }, + { url: "/bug510359", server: "1", expected: "1" }, +]; + +function setupChannel(suffix, value) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.requestMethod = "GET"; + httpChan.setRequestHeader("x-request", value, false); + httpChan.setRequestHeader("Cookie", "c=" + value, false); + return httpChan; +} + +function triggerNextTest() { + var channel = setupChannel(tests[index].url, tests[index].server); + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null)); +} + +function checkValueAndTrigger(request, data, ctx) { + Assert.equal(tests[index].expected, data); + + if (index < tests.length - 1) { + index++; + triggerNextTest(); + } else { + httpserver.stop(do_test_finished); + } +} + +function run_test() { + httpserver.registerPathHandler("/bug510359", handler); + httpserver.start(-1); + + // clear cache + evict_cache_entries(); + + triggerNextTest(); + + do_test_pending(); +} + +function handler(metadata, response) { + try { + metadata.getHeader("If-Modified-Since"); + response.setStatusLine(metadata.httpVersion, 500, "Failed"); + var msg = "Client should not set If-Modified-Since header"; + response.bodyOutputStream.write(msg, msg.length); + } catch (ex) { + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Last-Modified", getDateString(-1), false); + response.setHeader("Vary", "Cookie", false); + var body = metadata.getHeader("x-request"); + response.bodyOutputStream.write(body, body.length); + } +} + +function getDateString(yearDelta) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + (d.getUTCFullYear() + yearDelta) + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_bug526789.js b/netwerk/test/unit/test_bug526789.js new file mode 100644 index 0000000000..ec80249a3c --- /dev/null +++ b/netwerk/test/unit/test_bug526789.js @@ -0,0 +1,289 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async () => { + var cm = Services.cookies; + var expiry = (Date.now() + 1000) * 1000; + + cm.removeAll(); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref("dom.security.https_first", false); + + // test that variants of 'baz.com' get normalized appropriately, but that + // malformed hosts are rejected + cm.add( + "baz.com", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(cm.countCookiesFromHost("baz.com"), 1); + Assert.equal(cm.countCookiesFromHost("BAZ.com"), 1); + Assert.equal(cm.countCookiesFromHost(".baz.com"), 1); + Assert.equal(cm.countCookiesFromHost("baz.com."), 0); + Assert.equal(cm.countCookiesFromHost(".baz.com."), 0); + do_check_throws(function () { + cm.countCookiesFromHost("baz.com.."); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function () { + cm.countCookiesFromHost("baz..com"); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function () { + cm.countCookiesFromHost("..baz.com"); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + cm.remove("BAZ.com.", "foo", "/", {}); + Assert.equal(cm.countCookiesFromHost("baz.com"), 1); + cm.remove("baz.com", "foo", "/", {}); + Assert.equal(cm.countCookiesFromHost("baz.com"), 0); + + // Test that 'baz.com' and 'baz.com.' are treated differently + cm.add( + "baz.com.", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(cm.countCookiesFromHost("baz.com"), 0); + Assert.equal(cm.countCookiesFromHost("BAZ.com"), 0); + Assert.equal(cm.countCookiesFromHost(".baz.com"), 0); + Assert.equal(cm.countCookiesFromHost("baz.com."), 1); + Assert.equal(cm.countCookiesFromHost(".baz.com."), 1); + cm.remove("baz.com", "foo", "/", {}); + Assert.equal(cm.countCookiesFromHost("baz.com."), 1); + cm.remove("baz.com.", "foo", "/", {}); + Assert.equal(cm.countCookiesFromHost("baz.com."), 0); + + // test that domain cookies are illegal for IP addresses, aliases such as + // 'localhost', and eTLD's such as 'co.uk' + cm.add( + "192.168.0.1", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(cm.countCookiesFromHost("192.168.0.1"), 1); + Assert.equal(cm.countCookiesFromHost("192.168.0.1."), 0); + do_check_throws(function () { + cm.countCookiesFromHost(".192.168.0.1"); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function () { + cm.countCookiesFromHost(".192.168.0.1."); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + cm.add( + "localhost", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(cm.countCookiesFromHost("localhost"), 1); + Assert.equal(cm.countCookiesFromHost("localhost."), 0); + do_check_throws(function () { + cm.countCookiesFromHost(".localhost"); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function () { + cm.countCookiesFromHost(".localhost."); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + cm.add( + "co.uk", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(cm.countCookiesFromHost("co.uk"), 1); + Assert.equal(cm.countCookiesFromHost("co.uk."), 0); + do_check_throws(function () { + cm.countCookiesFromHost(".co.uk"); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function () { + cm.countCookiesFromHost(".co.uk."); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + cm.removeAll(); + + CookieXPCShellUtils.createServer({ + hosts: ["baz.com", "192.168.0.1", "localhost", "co.uk", "foo.com"], + }); + + var uri = NetUtil.newURI("http://baz.com/"); + Services.scriptSecurityManager.createContentPrincipal(uri, {}); + + Assert.equal(uri.asciiHost, "baz.com"); + + await CookieXPCShellUtils.setCookieToDocument(uri.spec, "foo=bar"); + const docCookies = await CookieXPCShellUtils.getCookieStringFromDocument( + uri.spec + ); + Assert.equal(docCookies, "foo=bar"); + + Assert.equal(cm.countCookiesFromHost(""), 0); + do_check_throws(function () { + cm.countCookiesFromHost("."); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function () { + cm.countCookiesFromHost(".."); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + var cookies = cm.getCookiesFromHost("", {}); + Assert.ok(!cookies.length); + do_check_throws(function () { + cm.getCookiesFromHost(".", {}); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function () { + cm.getCookiesFromHost("..", {}); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + cookies = cm.getCookiesFromHost("baz.com", {}); + Assert.equal(cookies.length, 1); + Assert.equal(cookies[0].name, "foo"); + cookies = cm.getCookiesFromHost("", {}); + Assert.ok(!cookies.length); + do_check_throws(function () { + cm.getCookiesFromHost(".", {}); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function () { + cm.getCookiesFromHost("..", {}); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + cm.removeAll(); + + // test that an empty host to add() or remove() works, + // but a host of '.' doesn't + cm.add( + "", + "/", + "foo2", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(getCookieCount(), 1); + do_check_throws(function () { + cm.add( + ".", + "/", + "foo3", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + Assert.equal(getCookieCount(), 1); + + cm.remove("", "foo2", "/", {}); + Assert.equal(getCookieCount(), 0); + do_check_throws(function () { + cm.remove(".", "foo3", "/", {}); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + // test that the 'domain' attribute accepts a leading dot for IP addresses, + // aliases such as 'localhost', and eTLD's such as 'co.uk'; but that the + // resulting cookie is for the exact host only. + await testDomainCookie("http://192.168.0.1/", "192.168.0.1"); + await testDomainCookie("http://localhost/", "localhost"); + await testDomainCookie("http://co.uk/", "co.uk"); + + // Test that trailing dots are treated differently for purposes of the + // 'domain' attribute when using setCookieStringFromDocument. + await testTrailingDotCookie("http://localhost/", "localhost"); + await testTrailingDotCookie("http://foo.com/", "foo.com"); + + cm.removeAll(); +}); + +function getCookieCount() { + var cm = Services.cookies; + return cm.cookies.length; +} + +async function testDomainCookie(uriString, domain) { + var cm = Services.cookies; + + cm.removeAll(); + + await CookieXPCShellUtils.setCookieToDocument( + uriString, + "foo=bar; domain=" + domain + ); + + var cookies = cm.getCookiesFromHost(domain, {}); + Assert.ok(cookies.length); + Assert.equal(cookies[0].host, domain); + cm.removeAll(); + + await CookieXPCShellUtils.setCookieToDocument( + uriString, + "foo=bar; domain=." + domain + ); + + cookies = cm.getCookiesFromHost(domain, {}); + Assert.ok(cookies.length); + Assert.equal(cookies[0].host, domain); + cm.removeAll(); +} + +async function testTrailingDotCookie(uriString, domain) { + var cm = Services.cookies; + + cm.removeAll(); + + await CookieXPCShellUtils.setCookieToDocument( + uriString, + "foo=bar; domain=" + domain + "/" + ); + + Assert.equal(cm.countCookiesFromHost(domain), 0); + Assert.equal(cm.countCookiesFromHost(domain + "."), 0); + cm.removeAll(); + Services.prefs.clearUserPref("dom.security.https_first"); +} diff --git a/netwerk/test/unit/test_bug528292.js b/netwerk/test/unit/test_bug528292.js new file mode 100644 index 0000000000..19c9e8ff70 --- /dev/null +++ b/netwerk/test/unit/test_bug528292.js @@ -0,0 +1,85 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const sentCookieVal = "foo=bar"; +const responseBody = "response body"; + +XPCOMUtils.defineLazyGetter(this, "baseURL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +const preRedirectPath = "/528292/pre-redirect"; + +XPCOMUtils.defineLazyGetter(this, "preRedirectURL", function () { + return baseURL + preRedirectPath; +}); + +const postRedirectPath = "/528292/post-redirect"; + +XPCOMUtils.defineLazyGetter(this, "postRedirectURL", function () { + return baseURL + postRedirectPath; +}); + +var httpServer = null; +var receivedCookieVal = null; + +function preRedirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Location", postRedirectURL, false); +} + +function postRedirectHandler(metadata, response) { + receivedCookieVal = metadata.getHeader("Cookie"); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +add_task(async () => { + // Start the HTTP server. + httpServer = new HttpServer(); + httpServer.registerPathHandler(preRedirectPath, preRedirectHandler); + httpServer.registerPathHandler(postRedirectPath, postRedirectHandler); + httpServer.start(-1); + + if (!inChildProcess()) { + // Disable third-party cookies in general. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 1); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + // Set up a channel with forceAllowThirdPartyCookie set to true. We'll use + // the channel both to set a cookie and then to load the pre-redirect URI. + var chan = NetUtil.newChannel({ + uri: preRedirectURL, + loadUsingSystemPrincipal: true, + }) + .QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + chan.forceAllowThirdPartyCookie = true; + + // Set a cookie on one of the URIs. It doesn't matter which one, since + // they're both from the same host, which is enough for the cookie service + // to send the cookie with both requests. + var postRedirectURI = Services.io.newURI(postRedirectURL); + + await CookieXPCShellUtils.setCookieToDocument( + postRedirectURI.spec, + sentCookieVal + ); + + // Load the pre-redirect URI. + await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null)); + }); + + Assert.equal(receivedCookieVal, sentCookieVal); + httpServer.stop(do_test_finished); +}); diff --git a/netwerk/test/unit/test_bug536324_64bit_content_length.js b/netwerk/test/unit/test_bug536324_64bit_content_length.js new file mode 100644 index 0000000000..9f60d7ac00 --- /dev/null +++ b/netwerk/test/unit/test_bug536324_64bit_content_length.js @@ -0,0 +1,64 @@ +/* Test to ensure our 64-bit content length implementation works, at least for + a simple HTTP case */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// This C-L is significantly larger than (U)INT32_MAX, to make sure we do +// 64-bit properly. +const CONTENT_LENGTH = "1152921504606846975"; + +var httpServer = null; + +var listener = { + onStartRequest(req) {}, + + onDataAvailable(req, stream, off, count) { + Assert.equal(req.getResponseHeader("Content-Length"), CONTENT_LENGTH); + + // We're done here, cancel the channel + req.cancel(Cr.NS_BINDING_ABORTED); + }, + + onStopRequest(req, stat) { + httpServer.stop(do_test_finished); + }, +}; + +function hugeContentLength(metadata, response) { + var text = "abcdefghijklmnopqrstuvwxyz"; + var bytes_written = 0; + + response.seizePower(); + + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Length: " + CONTENT_LENGTH + "\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + + // Write enough data to ensure onDataAvailable gets called + while (bytes_written < 4096) { + response.write(text); + bytes_written += text.length; + } + + response.finish(); +} + +function test_hugeContentLength() { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpServer.identity.primaryPort + "/", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.asyncOpen(listener); +} + +add_test(test_hugeContentLength); + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/", hugeContentLength); + httpServer.start(-1); + run_next_test(); +} diff --git a/netwerk/test/unit/test_bug540566.js b/netwerk/test/unit/test_bug540566.js new file mode 100644 index 0000000000..24260421ad --- /dev/null +++ b/netwerk/test/unit/test_bug540566.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +function continue_test(status, entry) { + Assert.equal(status, Cr.NS_OK); + // TODO - mayhemer: remove this tests completely + // entry.deviceID; + // if the above line does not crash, the test was successful + do_test_finished(); +} + +function run_test() { + asyncOpenCacheEntry( + "http://some.key/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + continue_test + ); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug553970.js b/netwerk/test/unit/test_bug553970.js new file mode 100644 index 0000000000..adb45a2ee9 --- /dev/null +++ b/netwerk/test/unit/test_bug553970.js @@ -0,0 +1,50 @@ +"use strict"; + +function makeURL(spec) { + return Services.io.newURI(spec).QueryInterface(Ci.nsIURL); +} + +// Checks that nsIURL::GetRelativeSpec does what it claims to do. +function run_test() { + // Elements of tests have the form [this.spec, aURIToCompare.spec, expectedResult]. + let tests = [ + [ + "http://mozilla.org/", + "http://www.mozilla.org/", + "http://www.mozilla.org/", + ], + [ + "http://mozilla.org/", + "http://www.mozilla.org", + "http://www.mozilla.org/", + ], + ["http://foo.com/bar/", "http://foo.com:80/bar/", ""], + ["http://foo.com/", "http://foo.com/a.htm#b", "a.htm#b"], + ["http://foo.com/a/b/", "http://foo.com/c", "../../c"], + ["http://foo.com/a?b/c/", "http://foo.com/c", "c"], + ["http://foo.com/a#b/c/", "http://foo.com/c", "c"], + ["http://foo.com/a;p?b/c/", "http://foo.com/c", "c"], + ["http://foo.com/a/b?c/d/", "http://foo.com/c", "../c"], + ["http://foo.com/a/b#c/d/", "http://foo.com/c", "../c"], + ["http://foo.com/a/b;p?c/d/", "http://foo.com/c", "../c"], + ["http://foo.com/a/b/c?d/e/", "http://foo.com/f", "../../f"], + ["http://foo.com/a/b/c#d/e/", "http://foo.com/f", "../../f"], + ["http://foo.com/a/b/c;p?d/e/", "http://foo.com/f", "../../f"], + ["http://foo.com/a?b/c/", "http://foo.com/c/d", "c/d"], + ["http://foo.com/a#b/c/", "http://foo.com/c/d", "c/d"], + ["http://foo.com/a;p?b/c/", "http://foo.com/c/d", "c/d"], + ["http://foo.com/a/b?c/d/", "http://foo.com/c/d", "../c/d"], + ["http://foo.com/a/b#c/d/", "http://foo.com/c/d", "../c/d"], + ["http://foo.com/a/b;p?c/d/", "http://foo.com/c/d", "../c/d"], + ["http://foo.com/a/b/c?d/e/", "http://foo.com/f/g/", "../../f/g/"], + ["http://foo.com/a/b/c#d/e/", "http://foo.com/f/g/", "../../f/g/"], + ["http://foo.com/a/b/c;p?d/e/", "http://foo.com/f/g/", "../../f/g/"], + ]; + + for (var i = 0; i < tests.length; i++) { + let url1 = makeURL(tests[i][0]); + let url2 = makeURL(tests[i][1]); + let expected = tests[i][2]; + Assert.equal(expected, url1.getRelativeSpec(url2)); + } +} diff --git a/netwerk/test/unit/test_bug561042.js b/netwerk/test/unit/test_bug561042.js new file mode 100644 index 0000000000..928518139d --- /dev/null +++ b/netwerk/test/unit/test_bug561042.js @@ -0,0 +1,42 @@ +/* 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 { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const SERVER_PORT = 8080; +const baseURL = "http://localhost:" + SERVER_PORT + "/"; + +var cookie = ""; +for (let i = 0; i < 10000; i++) { + cookie += " big cookie"; +} + +var listener = { + onStartRequest(request) {}, + + onDataAvailable(request, stream) {}, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + server.stop(do_test_finished); + }, +}; + +var server = new HttpServer(); +function run_test() { + server.start(SERVER_PORT); + server.registerPathHandler("/", function (metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Set-Cookie", "BigCookie=" + cookie, false); + response.write("Hello world"); + }); + var chan = NetUtil.newChannel({ + uri: baseURL, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.asyncOpen(listener); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug561276.js b/netwerk/test/unit/test_bug561276.js new file mode 100644 index 0000000000..03ff7b3e35 --- /dev/null +++ b/netwerk/test/unit/test_bug561276.js @@ -0,0 +1,64 @@ +// +// Verify that we hit the net if we discover a cycle of redirects +// coming from cache. +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var iteration = 0; + +function setupChannel(suffix) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.requestMethod = "GET"; + return httpChan; +} + +function checkValueAndTrigger(request, data, ctx) { + Assert.equal("Ok", data); + httpserver.stop(do_test_finished); +} + +function run_test() { + httpserver.registerPathHandler("/redirect1", redirectHandler1); + httpserver.registerPathHandler("/redirect2", redirectHandler2); + httpserver.start(-1); + + // clear cache + evict_cache_entries(); + + // load first time + var channel = setupChannel("/redirect1"); + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null)); + + do_test_pending(); +} + +function redirectHandler1(metadata, response) { + // first time we return a cacheable 302 pointing to next redirect + if (iteration < 1) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Cache-Control", "max-age=600", false); + response.setHeader("Location", "/redirect2", false); + + // next time called we return 200 + } else { + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Cache-Control", "max-age=600", false); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); + } + iteration += 1; +} + +function redirectHandler2(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Cache-Control", "max-age=600", false); + response.setHeader("Location", "/redirect1", false); +} diff --git a/netwerk/test/unit/test_bug580508.js b/netwerk/test/unit/test_bug580508.js new file mode 100644 index 0000000000..a17f59b334 --- /dev/null +++ b/netwerk/test/unit/test_bug580508.js @@ -0,0 +1,31 @@ +"use strict"; + +var ioService = Services.io; +var resProt = ioService + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + +function run_test() { + // Define a resource:// alias that points to another resource:// URI. + let greModulesURI = ioService.newURI("resource://gre/modules/"); + resProt.setSubstitution("my-gre-modules", greModulesURI); + + // When we ask for the alias, we should not get the resource:// + // URI that we registered it for but the original file URI. + let greFileSpec = ioService.newURI( + "modules/", + null, + resProt.getSubstitution("gre") + ).spec; + let aliasURI = resProt.getSubstitution("my-gre-modules"); + Assert.equal(aliasURI.spec, greFileSpec); + + // Resolving URIs using the original resource path and the alias + // should yield the same result. + let greNetUtilURI = ioService.newURI("resource://gre/modules/NetUtil.jsm"); + let myNetUtilURI = ioService.newURI("resource://my-gre-modules/NetUtil.jsm"); + Assert.equal( + resProt.resolveURI(greNetUtilURI), + resProt.resolveURI(myNetUtilURI) + ); +} diff --git a/netwerk/test/unit/test_bug586908.js b/netwerk/test/unit/test_bug586908.js new file mode 100644 index 0000000000..0d17c04b41 --- /dev/null +++ b/netwerk/test/unit/test_bug586908.js @@ -0,0 +1,101 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +var httpserv = null; + +XPCOMUtils.defineLazyGetter(this, "systemSettings", function () { + return { + QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]), + + mainThreadOnly: true, + PACURI: "http://localhost:" + httpserv.identity.primaryPort + "/redirect", + getProxyForURI(aURI) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + }; +}); + +function checkValue(request, data, ctx) { + Assert.ok(called); + Assert.equal("ok", data); + httpserv.stop(do_test_finished); +} + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/redirect", redirect); + httpserv.registerPathHandler("/pac", pac); + httpserv.registerPathHandler("/target", target); + httpserv.start(-1); + + MockRegistrar.register( + "@mozilla.org/system-proxy-settings;1", + systemSettings + ); + + // Ensure we're using system-properties + Services.prefs.setIntPref( + "network.proxy.type", + Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM + ); + + // clear cache + evict_cache_entries(); + + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/target" + ); + chan.asyncOpen(new ChannelListener(checkValue, null)); + + do_test_pending(); +} + +var called = false, + failed = false; +function redirect(metadata, response) { + // If called second time, just return the PAC but set failed-flag + if (called) { + failed = true; + pac(metadata, response); + return; + } + + called = true; + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Location", "/pac", false); + var body = "Moved\n"; + response.bodyOutputStream.write(body, body.length); +} + +function pac(metadata, response) { + var PAC = 'function FindProxyForURL(url, host) { return "DIRECT"; }'; + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader( + "Content-Type", + "application/x-ns-proxy-autoconfig", + false + ); + response.bodyOutputStream.write(PAC, PAC.length); +} + +function target(metadata, response) { + var retval = "ok"; + if (failed) { + retval = "failed"; + } + + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(retval, retval.length); +} diff --git a/netwerk/test/unit/test_bug596443.js b/netwerk/test/unit/test_bug596443.js new file mode 100644 index 0000000000..8c6896f41c --- /dev/null +++ b/netwerk/test/unit/test_bug596443.js @@ -0,0 +1,115 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +var httpserver = new HttpServer(); + +var expectedOnStopRequests = 3; + +function setupChannel(suffix, xRequest, flags) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + if (flags) { + chan.loadFlags |= flags; + } + + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.setRequestHeader("x-request", xRequest, false); + + return httpChan; +} + +function Listener(response) { + this._response = response; +} +Listener.prototype = { + _response: null, + _buffer: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + this._buffer = ""; + }, + onDataAvailable(request, stream, offset, count) { + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + onStopRequest(request, status) { + Assert.equal(this._buffer, this._response); + if (--expectedOnStopRequests == 0) { + do_timeout(10, function () { + httpserver.stop(do_test_finished); + }); + } + }, +}; + +function run_test() { + httpserver.registerPathHandler("/bug596443", handler); + httpserver.start(-1); + + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + // make sure we have a profile so we can use the disk-cache + do_get_profile(); + + // clear cache + evict_cache_entries(); + + httpProtocolHandler.EnsureHSTSDataReady().then(function () { + var ch0 = setupChannel( + "/bug596443", + "Response0", + Ci.nsIRequest.LOAD_BYPASS_CACHE + ); + ch0.asyncOpen(new Listener("Response0")); + + var ch1 = setupChannel( + "/bug596443", + "Response1", + Ci.nsIRequest.LOAD_BYPASS_CACHE + ); + ch1.asyncOpen(new Listener("Response1")); + + var ch2 = setupChannel("/bug596443", "Should not be used"); + ch2.asyncOpen(new Listener("Response1")); // Note param: we expect this to come from cache + }); + + do_test_pending(); +} + +function triggerHandlers() { + do_timeout(100, handlers[1]); + do_timeout(100, handlers[0]); +} + +var handlers = []; +function handler(metadata, response) { + var func = function (body) { + return function () { + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Length", "" + body.length, false); + response.setHeader("Cache-Control", "max-age=600", false); + response.bodyOutputStream.write(body, body.length); + response.finish(); + }; + }; + + response.processAsync(); + var request = metadata.getHeader("x-request"); + handlers.push(func(request)); + + if (handlers.length > 1) { + triggerHandlers(); + } +} diff --git a/netwerk/test/unit/test_bug618835.js b/netwerk/test/unit/test_bug618835.js new file mode 100644 index 0000000000..f3c4a4b86a --- /dev/null +++ b/netwerk/test/unit/test_bug618835.js @@ -0,0 +1,122 @@ +// +// If a response to a non-safe HTTP request-method contains the Location- or +// Content-Location header, we must make sure to invalidate any cached entry +// representing the URIs pointed to by either header. RFC 2616 section 13.10 +// +// This test uses 3 URIs: "/post" is the target of a POST-request and always +// redirects (301) to "/redirect". The URIs "/redirect" and "/cl" both counts +// the number of loads from the server (handler). The response from "/post" +// always contains the headers "Location: /redirect" and "Content-Location: +// /cl", whose cached entries are to be invalidated. The tests verifies that +// "/redirect" and "/cl" are loaded from server the expected number of times. +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserv; + +function setupChannel(path) { + return NetUtil.newChannel({ + uri: path, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +// Verify that Content-Location-URI has been loaded once, load post_target +function InitialListener() {} +InitialListener.prototype = { + onStartRequest(request) {}, + onStopRequest(request, status) { + Assert.equal(1, numberOfCLHandlerCalls); + executeSoon(function () { + var channel = setupChannel( + "http://localhost:" + httpserv.identity.primaryPort + "/post" + ); + channel.requestMethod = "POST"; + channel.asyncOpen(new RedirectingListener()); + }); + }, +}; + +// Verify that Location-URI has been loaded once, reload post_target +function RedirectingListener() {} +RedirectingListener.prototype = { + onStartRequest(request) {}, + onStopRequest(request, status) { + Assert.equal(1, numberOfHandlerCalls); + executeSoon(function () { + var channel = setupChannel( + "http://localhost:" + httpserv.identity.primaryPort + "/post" + ); + channel.requestMethod = "POST"; + channel.asyncOpen(new VerifyingListener()); + }); + }, +}; + +// Verify that Location-URI has been loaded twice (cached entry invalidated), +// reload Content-Location-URI +function VerifyingListener() {} +VerifyingListener.prototype = { + onStartRequest(request) {}, + onStopRequest(request, status) { + Assert.equal(2, numberOfHandlerCalls); + var channel = setupChannel( + "http://localhost:" + httpserv.identity.primaryPort + "/cl" + ); + channel.asyncOpen(new FinalListener()); + }, +}; + +// Verify that Location-URI has been loaded twice (cached entry invalidated), +// stop test +function FinalListener() {} +FinalListener.prototype = { + onStartRequest(request) {}, + onStopRequest(request, status) { + Assert.equal(2, numberOfCLHandlerCalls); + httpserv.stop(do_test_finished); + }, +}; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/cl", content_location); + httpserv.registerPathHandler("/post", post_target); + httpserv.registerPathHandler("/redirect", redirect_target); + httpserv.start(-1); + + // Clear cache + evict_cache_entries(); + + // Load Content-Location URI into cache and start the chain of loads + var channel = setupChannel( + "http://localhost:" + httpserv.identity.primaryPort + "/cl" + ); + channel.asyncOpen(new InitialListener()); + + do_test_pending(); +} + +var numberOfCLHandlerCalls = 0; +function content_location(metadata, response) { + numberOfCLHandlerCalls++; + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Cache-Control", "max-age=360000", false); +} + +function post_target(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", "/redirect", false); + response.setHeader("Content-Location", "/cl", false); + response.setHeader("Cache-Control", "max-age=360000", false); +} + +var numberOfHandlerCalls = 0; +function redirect_target(metadata, response) { + numberOfHandlerCalls++; + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Cache-Control", "max-age=360000", false); +} diff --git a/netwerk/test/unit/test_bug633743.js b/netwerk/test/unit/test_bug633743.js new file mode 100644 index 0000000000..eb34de9fe9 --- /dev/null +++ b/netwerk/test/unit/test_bug633743.js @@ -0,0 +1,193 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const VALUE_HDR_NAME = "X-HTTP-VALUE-HEADER"; +const VARY_HDR_NAME = "X-HTTP-VARY-HEADER"; +const CACHECTRL_HDR_NAME = "X-CACHE-CONTROL-HEADER"; + +var httpserver = null; + +function make_channel(flags, vary, value) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + "/bug633743", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return chan.QueryInterface(Ci.nsIHttpChannel); +} + +function Test(flags, varyHdr, sendValue, expectValue, cacheHdr) { + this._flags = flags; + this._varyHdr = varyHdr; + this._sendVal = sendValue; + this._expectVal = expectValue; + this._cacheHdr = cacheHdr; +} + +Test.prototype = { + _buffer: "", + _flags: null, + _varyHdr: null, + _sendVal: null, + _expectVal: null, + _cacheHdr: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) {}, + + onDataAvailable(request, stream, offset, count) { + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + + onStopRequest(request, status) { + Assert.equal(this._buffer, this._expectVal); + do_timeout(0, run_next_test); + }, + + run() { + var channel = make_channel(); + channel.loadFlags = this._flags; + channel.setRequestHeader(VALUE_HDR_NAME, this._sendVal, false); + channel.setRequestHeader(VARY_HDR_NAME, this._varyHdr, false); + if (this._cacheHdr) { + channel.setRequestHeader(CACHECTRL_HDR_NAME, this._cacheHdr, false); + } + + channel.asyncOpen(this); + }, +}; + +var gTests = [ + // Test LOAD_FROM_CACHE: Load cache-entry + new Test( + Ci.nsIRequest.LOAD_NORMAL, + "entity-initial", // hdr-value used to vary + "request1", // echoed by handler + "request1" // value expected to receive in channel + ), + // Verify that it was cached + new Test( + Ci.nsIRequest.LOAD_NORMAL, + "entity-initial", // hdr-value used to vary + "fresh value with LOAD_NORMAL", // echoed by handler + "request1" // value expected to receive in channel + ), + // Load same entity with LOAD_FROM_CACHE-flag + new Test( + Ci.nsIRequest.LOAD_FROM_CACHE, + "entity-initial", // hdr-value used to vary + "fresh value with LOAD_FROM_CACHE", // echoed by handler + "request1" // value expected to receive in channel + ), + // Load different entity with LOAD_FROM_CACHE-flag + new Test( + Ci.nsIRequest.LOAD_FROM_CACHE, + "entity-l-f-c", // hdr-value used to vary + "request2", // echoed by handler + "request2" // value expected to receive in channel + ), + // Verify that new value was cached + new Test( + Ci.nsIRequest.LOAD_NORMAL, + "entity-l-f-c", // hdr-value used to vary + "fresh value with LOAD_NORMAL", // echoed by handler + "request2" // value expected to receive in channel + ), + + // Test VALIDATE_NEVER: Note previous cache-entry + new Test( + Ci.nsIRequest.VALIDATE_NEVER, + "entity-v-n", // hdr-value used to vary + "request3", // echoed by handler + "request3" // value expected to receive in channel + ), + // Verify that cache-entry was replaced + new Test( + Ci.nsIRequest.LOAD_NORMAL, + "entity-v-n", // hdr-value used to vary + "fresh value with LOAD_NORMAL", // echoed by handler + "request3" // value expected to receive in channel + ), + + // Test combination VALIDATE_NEVER && no-store: Load new cache-entry + new Test( + Ci.nsIRequest.LOAD_NORMAL, + "entity-2", // hdr-value used to vary + "request4", // echoed by handler + "request4", // value expected to receive in channel + "no-store" // set no-store on response + ), + // Ensure we validate without IMS header in this case (verified in handler) + new Test( + Ci.nsIRequest.VALIDATE_NEVER, + "entity-2-v-n", // hdr-value used to vary + "request5", // echoed by handler + "request5" // value expected to receive in channel + ), + + // Test VALIDATE-ALWAYS: Load new entity + new Test( + Ci.nsIRequest.LOAD_NORMAL, + "entity-3", // hdr-value used to vary + "request6", // echoed by handler + "request6", // value expected to receive in channel + "no-cache" // set no-cache on response + ), + // Ensure we don't send IMS header also in this case (verified in handler) + new Test( + Ci.nsIRequest.VALIDATE_ALWAYS, + "entity-3-v-a", // hdr-value used to vary + "request7", // echoed by handler + "request7" // value expected to receive in channel + ), +]; + +function run_next_test() { + if (!gTests.length) { + httpserver.stop(do_test_finished); + return; + } + + var test = gTests.shift(); + test.run(); +} + +function handler(metadata, response) { + // None of the tests above should send an IMS + Assert.ok(!metadata.hasHeader("If-Modified-Since")); + + // Pick up requested value to echo + var hdr = "default value"; + try { + hdr = metadata.getHeader(VALUE_HDR_NAME); + } catch (ex) {} + + // Pick up requested cache-control header-value + var cctrlVal = "max-age=10000"; + try { + cctrlVal = metadata.getHeader(CACHECTRL_HDR_NAME); + } catch (ex) {} + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", cctrlVal, false); + response.setHeader("Vary", VARY_HDR_NAME, false); + response.setHeader("Last-Modified", "Tue, 15 Nov 1994 12:45:26 GMT", false); + response.bodyOutputStream.write(hdr, hdr.length); +} + +function run_test() { + // clear the cache + evict_cache_entries(); + + httpserver = new HttpServer(); + httpserver.registerPathHandler("/bug633743", handler); + httpserver.start(-1); + + run_next_test(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug650522.js b/netwerk/test/unit/test_bug650522.js new file mode 100644 index 0000000000..2ffcb5438a --- /dev/null +++ b/netwerk/test/unit/test_bug650522.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async () => { + Services.prefs.setBoolPref("network.cookie.sameSite.schemeful", false); + Services.prefs.setBoolPref("dom.security.https_first", false); + + var expiry = (Date.now() + 1000) * 1000; + + // Test our handling of host names with a single character at the beginning + // followed by a dot. + Services.cookies.add( + "e.com", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + Assert.equal(Services.cookies.countCookiesFromHost("e.com"), 1); + + CookieXPCShellUtils.createServer({ hosts: ["e.com"] }); + const cookies = await CookieXPCShellUtils.getCookieStringFromDocument( + "http://e.com/" + ); + Assert.equal(cookies, "foo=bar"); + Services.prefs.clearUserPref("dom.security.https_first"); +}); diff --git a/netwerk/test/unit/test_bug650995.js b/netwerk/test/unit/test_bug650995.js new file mode 100644 index 0000000000..4eff87c721 --- /dev/null +++ b/netwerk/test/unit/test_bug650995.js @@ -0,0 +1,185 @@ +// +// Test that "max_entry_size" prefs for disk- and memory-cache prevents +// caching resources with size out of bounds +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +do_get_profile(); + +const prefService = Services.prefs; + +const httpserver = new HttpServer(); + +// Repeats the given data until the total size is larger than 1K +function repeatToLargerThan1K(data) { + while (data.length <= 1024) { + data += data; + } + return data; +} + +function setupChannel(suffix, value) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.setRequestHeader("x-request", value, false); + + return httpChan; +} + +var tests = [ + new InitializeCacheDevices(true, false), // enable and create mem-device + new TestCacheEntrySize( + function () { + prefService.setIntPref("browser.cache.memory.max_entry_size", 1); + }, + "012345", + "9876543210", + "012345" + ), // expect cached value + new TestCacheEntrySize( + function () { + prefService.setIntPref("browser.cache.memory.max_entry_size", 1); + }, + "0123456789a", + "9876543210", + "9876543210" + ), // expect fresh value + new TestCacheEntrySize( + function () { + prefService.setIntPref("browser.cache.memory.max_entry_size", -1); + }, + "0123456789a", + "9876543210", + "0123456789a" + ), // expect cached value + + new InitializeCacheDevices(false, true), // enable and create disk-device + new TestCacheEntrySize( + function () { + prefService.setIntPref("browser.cache.disk.max_entry_size", 1); + }, + "012345", + "9876543210", + "012345" + ), // expect cached value + new TestCacheEntrySize( + function () { + prefService.setIntPref("browser.cache.disk.max_entry_size", 1); + }, + "0123456789a", + "9876543210", + "9876543210" + ), // expect fresh value + new TestCacheEntrySize( + function () { + prefService.setIntPref("browser.cache.disk.max_entry_size", -1); + }, + "0123456789a", + "9876543210", + "0123456789a" + ), // expect cached value +]; + +function nextTest() { + // We really want each test to be self-contained. Make sure cache is + // cleared and also let all operations finish before starting a new test + syncWithCacheIOThread(function () { + Services.cache2.clear(); + syncWithCacheIOThread(runNextTest); + }); +} + +function runNextTest() { + var aTest = tests.shift(); + if (!aTest) { + httpserver.stop(do_test_finished); + return; + } + executeSoon(function () { + aTest.start(); + }); +} + +// Just make sure devices are created +function InitializeCacheDevices(memDevice, diskDevice) { + this.start = function () { + prefService.setBoolPref("browser.cache.memory.enable", memDevice); + if (memDevice) { + let cap = prefService.getIntPref("browser.cache.memory.capacity", 0); + if (cap == 0) { + prefService.setIntPref("browser.cache.memory.capacity", 1024); + } + } + prefService.setBoolPref("browser.cache.disk.enable", diskDevice); + if (diskDevice) { + let cap = prefService.getIntPref("browser.cache.disk.capacity", 0); + if (cap == 0) { + prefService.setIntPref("browser.cache.disk.capacity", 1024); + } + } + var channel = setupChannel("/bug650995", "Initial value"); + channel.asyncOpen(new ChannelListener(nextTest, null)); + }; +} + +function TestCacheEntrySize( + setSizeFunc, + firstRequest, + secondRequest, + secondExpectedReply +) { + // Initially, this test used 10 bytes as the limit for caching entries. + // Since we now use 1K granularity we have to extend lengths to be larger + // than 1K if it is larger than 10 + if (firstRequest.length > 10) { + firstRequest = repeatToLargerThan1K(firstRequest); + } + if (secondExpectedReply.length > 10) { + secondExpectedReply = repeatToLargerThan1K(secondExpectedReply); + } + + this.start = function () { + setSizeFunc(); + var channel = setupChannel("/bug650995", firstRequest); + channel.asyncOpen(new ChannelListener(this.initialLoad, this)); + }; + this.initialLoad = function (request, data, ctx) { + Assert.equal(firstRequest, data); + var channel = setupChannel("/bug650995", secondRequest); + executeSoon(function () { + channel.asyncOpen(new ChannelListener(ctx.testAndTriggerNext, ctx)); + }); + }; + this.testAndTriggerNext = function (request, data, ctx) { + Assert.equal(secondExpectedReply, data); + executeSoon(nextTest); + }; +} + +function run_test() { + httpserver.registerPathHandler("/bug650995", handler); + httpserver.start(-1); + + prefService.setBoolPref("network.http.rcwn.enabled", false); + + nextTest(); + do_test_pending(); +} + +function handler(metadata, response) { + var body = "BOOM!"; + try { + body = metadata.getHeader("x-request"); + } catch (e) {} + + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "max-age=3600", false); + response.bodyOutputStream.write(body, body.length); +} diff --git a/netwerk/test/unit/test_bug652761.js b/netwerk/test/unit/test_bug652761.js new file mode 100644 index 0000000000..030f18a1fd --- /dev/null +++ b/netwerk/test/unit/test_bug652761.js @@ -0,0 +1,19 @@ +// This is just a crashtest for a url that is rejected at parse time (port 80,000) + +"use strict"; + +function run_test() { + // Bug 1301621 makes invalid ports throw + Assert.throws( + () => { + NetUtil.newChannel({ + uri: "http://localhost:80000/", + loadUsingSystemPrincipal: true, + }); + }, + /NS_ERROR_MALFORMED_URI/, + "invalid port" + ); + + do_test_finished(); +} diff --git a/netwerk/test/unit/test_bug654926.js b/netwerk/test/unit/test_bug654926.js new file mode 100644 index 0000000000..0f29a3bcf4 --- /dev/null +++ b/netwerk/test/unit/test_bug654926.js @@ -0,0 +1,91 @@ +"use strict"; + +function gen_1MiB() { + var i; + var data = "x"; + for (i = 0; i < 20; i++) { + data += data; + } + return data; +} + +function write_and_check(str, data, len) { + var written = str.write(data, len); + if (written != len) { + do_throw( + "str.write has not written all data!\n" + + " Expected: " + + len + + "\n" + + " Actual: " + + written + + "\n" + ); + } +} + +function write_datafile(status, entry) { + Assert.equal(status, Cr.NS_OK); + var data = gen_1MiB(); + var os = entry.openOutputStream(0, data.length); + + // write 2MiB + var i; + for (i = 0; i < 2; i++) { + write_and_check(os, data, data.length); + } + + os.close(); + entry.close(); + + // now change max_entry_size so that the existing entry is too big + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1024); + + // append to entry + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + append_datafile + ); +} + +function append_datafile(status, entry) { + Assert.equal(status, Cr.NS_OK); + var os = entry.openOutputStream(entry.dataSize, -1); + var data = gen_1MiB(); + + // append 1MiB + try { + write_and_check(os, data, data.length); + do_throw(); + } catch (ex) {} + + // closing the ostream should fail in this case + try { + os.close(); + do_throw(); + } catch (ex) {} + + entry.close(); + + do_test_finished(); +} + +function run_test() { + do_get_profile(); + + // clear the cache + evict_cache_entries(); + + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + write_datafile + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug654926_doom_and_read.js b/netwerk/test/unit/test_bug654926_doom_and_read.js new file mode 100644 index 0000000000..1ed618e04a --- /dev/null +++ b/netwerk/test/unit/test_bug654926_doom_and_read.js @@ -0,0 +1,82 @@ +"use strict"; + +function gen_1MiB() { + var i; + var data = "x"; + for (i = 0; i < 20; i++) { + data += data; + } + return data; +} + +function write_and_check(str, data, len) { + var written = str.write(data, len); + if (written != len) { + do_throw( + "str.write has not written all data!\n" + + " Expected: " + + len + + "\n" + + " Actual: " + + written + + "\n" + ); + } +} + +function write_datafile(status, entry) { + Assert.equal(status, Cr.NS_OK); + var data = gen_1MiB(); + var os = entry.openOutputStream(0, data.length); + + write_and_check(os, data, data.length); + + os.close(); + entry.close(); + + // open, doom, append, read + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + test_read_after_doom + ); +} + +function test_read_after_doom(status, entry) { + Assert.equal(status, Cr.NS_OK); + var data = gen_1MiB(); + var os = entry.openOutputStream(entry.dataSize, data.length); + + entry.asyncDoom(null); + write_and_check(os, data, data.length); + + os.close(); + + var is = entry.openInputStream(0); + pumpReadStream(is, function (read) { + Assert.equal(read.length, 2 * 1024 * 1024); + is.close(); + + entry.close(); + do_test_finished(); + }); +} + +function run_test() { + do_get_profile(); + + // clear the cache + evict_cache_entries(); + + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + write_datafile + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug654926_test_seek.js b/netwerk/test/unit/test_bug654926_test_seek.js new file mode 100644 index 0000000000..148e9f9043 --- /dev/null +++ b/netwerk/test/unit/test_bug654926_test_seek.js @@ -0,0 +1,76 @@ +"use strict"; + +function gen_1MiB() { + var i; + var data = "x"; + for (i = 0; i < 20; i++) { + data += data; + } + return data; +} + +function write_and_check(str, data, len) { + var written = str.write(data, len); + if (written != len) { + do_throw( + "str.write has not written all data!\n" + + " Expected: " + + len + + "\n" + + " Actual: " + + written + + "\n" + ); + } +} + +function write_datafile(status, entry) { + Assert.equal(status, Cr.NS_OK); + var data = gen_1MiB(); + var os = entry.openOutputStream(0, data.length); + + write_and_check(os, data, data.length); + + os.close(); + entry.close(); + + // try to open the entry for appending + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + open_for_readwrite + ); +} + +function open_for_readwrite(status, entry) { + Assert.equal(status, Cr.NS_OK); + var os = entry.openOutputStream(entry.dataSize, -1); + + // Opening the entry for appending data calls nsDiskCacheStreamIO::Seek() + // which initializes mFD. If no data is written then mBufDirty is false and + // mFD won't be closed in nsDiskCacheStreamIO::Flush(). + + os.close(); + entry.close(); + + do_test_finished(); +} + +function run_test() { + do_get_profile(); + + // clear the cache + evict_cache_entries(); + + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + write_datafile + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug659569.js b/netwerk/test/unit/test_bug659569.js new file mode 100644 index 0000000000..60a14765b2 --- /dev/null +++ b/netwerk/test/unit/test_bug659569.js @@ -0,0 +1,58 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); + +function setupChannel(suffix) { + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); +} + +function checkValueAndTrigger(request, data, ctx) { + Assert.equal("Ok", data); + httpserver.stop(do_test_finished); +} + +function run_test() { + // We don't want to have CookieJarSettings blocking this test. + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + httpserver.registerPathHandler("/redirect1", redirectHandler1); + httpserver.registerPathHandler("/redirect2", redirectHandler2); + httpserver.start(-1); + + // clear cache + evict_cache_entries(); + + // load first time + var channel = setupChannel("/redirect1"); + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null)); + do_test_pending(); +} + +function redirectHandler1(metadata, response) { + if (!metadata.hasHeader("Cookie")) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Cache-Control", "max-age=600", false); + response.setHeader("Location", "/redirect2?query", false); + response.setHeader("Set-Cookie", "MyCookie=1", false); + } else { + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); + } +} + +function redirectHandler2(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Location", "/redirect1", false); +} diff --git a/netwerk/test/unit/test_bug660066.js b/netwerk/test/unit/test_bug660066.js new file mode 100644 index 0000000000..2e7c060135 --- /dev/null +++ b/netwerk/test/unit/test_bug660066.js @@ -0,0 +1,53 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +"use strict"; + +const SIMPLEURI_SPEC = "data:text/plain,hello world"; +const BLOBURI_SPEC = "blob:123456"; + +function do_info(text, stack) { + if (!stack) { + stack = Components.stack.caller; + } + + dump( + "\n" + + "TEST-INFO | " + + stack.filename + + " | [" + + stack.name + + " : " + + stack.lineNumber + + "] " + + text + + "\n" + ); +} + +function do_check_uri_neq(uri1, uri2) { + do_info("Checking equality in forward direction..."); + Assert.ok(!uri1.equals(uri2)); + Assert.ok(!uri1.equalsExceptRef(uri2)); + + do_info("Checking equality in reverse direction..."); + Assert.ok(!uri2.equals(uri1)); + Assert.ok(!uri2.equalsExceptRef(uri1)); +} + +function run_test() { + var simpleURI = NetUtil.newURI(SIMPLEURI_SPEC); + var fileDataURI = NetUtil.newURI(BLOBURI_SPEC); + + do_info("Checking that " + SIMPLEURI_SPEC + " != " + BLOBURI_SPEC); + do_check_uri_neq(simpleURI, fileDataURI); + + do_info("Changing the nsSimpleURI spec to match the nsFileDataURI"); + simpleURI = simpleURI.mutate().setSpec(BLOBURI_SPEC).finalize(); + + do_info("Verifying that .spec matches"); + Assert.equal(simpleURI.spec, fileDataURI.spec); + + do_info( + "Checking that nsSimpleURI != nsFileDataURI despite their .spec matching" + ); + do_check_uri_neq(simpleURI, fileDataURI); +} diff --git a/netwerk/test/unit/test_bug667087.js b/netwerk/test/unit/test_bug667087.js new file mode 100644 index 0000000000..79589799e7 --- /dev/null +++ b/netwerk/test/unit/test_bug667087.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async () => { + Services.prefs.setBoolPref("dom.security.https_first", false); + var expiry = (Date.now() + 1000) * 1000; + + // Test our handling of host names with a single character consisting only + // of a single character + Services.cookies.add( + "a", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + Assert.equal(Services.cookies.countCookiesFromHost("a"), 1); + + CookieXPCShellUtils.createServer({ hosts: ["a"] }); + const cookies = await CookieXPCShellUtils.getCookieStringFromDocument( + "http://a/" + ); + Assert.equal(cookies, "foo=bar"); + Services.prefs.clearUserPref("dom.security.https_first"); +}); diff --git a/netwerk/test/unit/test_bug667818.js b/netwerk/test/unit/test_bug667818.js new file mode 100644 index 0000000000..9c6e495da0 --- /dev/null +++ b/netwerk/test/unit/test_bug667818.js @@ -0,0 +1,49 @@ +"use strict"; + +function makeURI(str) { + return Services.io.newURI(str); +} + +add_task(async () => { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + Services.prefs.setBoolPref("dom.security.https_first", false); + + var uri = makeURI("http://example.com/"); + var channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + Services.scriptSecurityManager.createContentPrincipal(uri, {}); + + CookieXPCShellUtils.createServer({ hosts: ["example.com"] }); + + // Try an expiration time before the epoch + + await CookieXPCShellUtils.setCookieToDocument( + uri.spec, + "test=test; path=/; domain=example.com; expires=Sun, 31-Dec-1899 16:00:00 GMT;" + ); + Assert.equal( + await CookieXPCShellUtils.getCookieStringFromDocument(uri.spec), + "" + ); + + // Now sanity check + Services.cookies.setCookieStringFromHttp( + uri, + "test2=test2; path=/; domain=example.com;", + channel + ); + + Assert.equal( + await CookieXPCShellUtils.getCookieStringFromDocument(uri.spec), + "test2=test2" + ); + Services.prefs.clearUserPref("dom.security.https_first"); +}); diff --git a/netwerk/test/unit/test_bug667907.js b/netwerk/test/unit/test_bug667907.js new file mode 100644 index 0000000000..0288267f8d --- /dev/null +++ b/netwerk/test/unit/test_bug667907.js @@ -0,0 +1,86 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; +var simplePath = "/simple"; +var normalPath = "/normal"; +var httpbody = "<html></html>"; + +XPCOMUtils.defineLazyGetter(this, "uri1", function () { + return "http://localhost:" + httpserver.identity.primaryPort + simplePath; +}); + +XPCOMUtils.defineLazyGetter(this, "uri2", function () { + return "http://localhost:" + httpserver.identity.primaryPort + normalPath; +}); + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var listener_proto = { + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + Assert.equal( + request.QueryInterface(Ci.nsIChannel).contentType, + this.contentType + ); + request.cancel(Cr.NS_BINDING_ABORTED); + }, + + onDataAvailable(request, stream, offset, count) { + do_throw("Unexpected onDataAvailable"); + }, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_BINDING_ABORTED); + this.termination_func(); + }, +}; + +function listener(contentType, termination_func) { + this.contentType = contentType; + this.termination_func = termination_func; +} +listener.prototype = listener_proto; + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler(simplePath, simpleHandler); + httpserver.registerPathHandler(normalPath, normalHandler); + httpserver.start(-1); + + var channel = make_channel(uri1); + channel.asyncOpen( + new listener("text/plain", function () { + run_test2(); + }) + ); + + do_test_pending(); +} + +function run_test2() { + var channel = make_channel(uri2); + channel.asyncOpen( + new listener("text/html", function () { + httpserver.stop(do_test_finished); + }) + ); +} + +function simpleHandler(metadata, response) { + response.seizePower(); + response.bodyOutputStream.write(httpbody, httpbody.length); + response.finish(); +} + +function normalHandler(metadata, response) { + response.bodyOutputStream.write(httpbody, httpbody.length); + response.finish(); +} diff --git a/netwerk/test/unit/test_bug669001.js b/netwerk/test/unit/test_bug669001.js new file mode 100644 index 0000000000..a9d55cd57d --- /dev/null +++ b/netwerk/test/unit/test_bug669001.js @@ -0,0 +1,176 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; +var path = "/bug699001"; + +XPCOMUtils.defineLazyGetter(this, "URI", function () { + return "http://localhost:" + httpServer.identity.primaryPort + path; +}); + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var fetched; + +// The test loads a resource that expires in one year, has an etag and varies only by User-Agent +// First we load it, then check we load it only from the cache w/o even checking with the server +// Then we modify our User-Agent and try it again +// We have to get a new content (even though with the same etag) and again on next load only from +// cache w/o accessing the server +// Goal is to check we've updated User-Agent request header in cache after we've got 304 response +// from the server + +var tests = [ + { + prepare() {}, + test(response) { + Assert.ok(fetched); + }, + }, + { + prepare() {}, + test(response) { + Assert.ok(!fetched); + }, + }, + { + prepare() { + setUA("A different User Agent"); + }, + test(response) { + Assert.ok(fetched); + }, + }, + { + prepare() {}, + test(response) { + Assert.ok(!fetched); + }, + }, + { + prepare() { + setUA("And another User Agent"); + }, + test(response) { + Assert.ok(fetched); + }, + }, + { + prepare() {}, + test(response) { + Assert.ok(!fetched); + }, + }, +]; + +function handler(metadata, response) { + if (metadata.hasHeader("If-None-Match")) { + response.setStatusLine(metadata.httpVersion, 304, "Not modified"); + } else { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain"); + + var body = "body"; + response.bodyOutputStream.write(body, body.length); + } + + fetched = true; + + response.setHeader("Expires", getDateString(+1)); + response.setHeader("Cache-Control", "private"); + response.setHeader("Vary", "User-Agent"); + response.setHeader("ETag", "1234"); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(path, handler); + httpServer.start(-1); + + do_test_pending(); + + nextTest(); +} + +function nextTest() { + fetched = false; + tests[0].prepare(); + + dump("Testing with User-Agent: " + getUA() + "\n"); + var chan = make_channel(URI); + + // Give the old channel a chance to close the cache entry first. + // XXX This is actually a race condition that might be considered a bug... + executeSoon(function () { + chan.asyncOpen(new ChannelListener(checkAndShiftTest, null)); + }); +} + +function checkAndShiftTest(request, response) { + tests[0].test(response); + + tests.shift(); + if (!tests.length) { + httpServer.stop(tearDown); + return; + } + + nextTest(); +} + +function tearDown() { + setUA(""); + do_test_finished(); +} + +// Helpers + +function getUA() { + var httphandler = Cc["@mozilla.org/network/protocol;1?name=http"].getService( + Ci.nsIHttpProtocolHandler + ); + return httphandler.userAgent; +} + +function setUA(value) { + Services.prefs.setCharPref("general.useragent.override", value); +} + +function getDateString(yearDelta) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + (d.getUTCFullYear() + yearDelta) + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_bug770243.js b/netwerk/test/unit/test_bug770243.js new file mode 100644 index 0000000000..dab621b2d7 --- /dev/null +++ b/netwerk/test/unit/test_bug770243.js @@ -0,0 +1,244 @@ +/* this test does the following: + Always requests the same resource, while for each request getting: + 1. 200 + ETag: "one" + 2. 401 followed by 200 + ETag: "two" + 3. 401 followed by 304 + 4. 407 followed by 200 + ETag: "three" + 5. 407 followed by 304 +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserv; + +function addCreds(scheme, host) { + var authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + authMgr.setAuthIdentity( + scheme, + host, + httpserv.identity.primaryPort, + "basic", + "secret", + "/", + "", + "user", + "pass" + ); +} + +function clearCreds() { + var authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + authMgr.clearAll(); +} + +function makeChan() { + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserv.identity.primaryPort + "/", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +// Array of handlers that are called one by one in response to expected requests + +var handlers = [ + // Test 1 + function (metadata, response) { + Assert.equal(metadata.hasHeader("Authorization"), false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("ETag", '"one"', false); + response.setHeader("Cache-control", "no-cache", false); + response.setHeader("Content-type", "text/plain", false); + var body = "Response body 1"; + response.bodyOutputStream.write(body, body.length); + }, + + // Test 2 + function (metadata, response) { + Assert.equal(metadata.hasHeader("Authorization"), false); + Assert.equal(metadata.getHeader("If-None-Match"), '"one"'); + response.setStatusLine(metadata.httpVersion, 401, "Authenticate"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + addCreds("http", "localhost"); + }, + function (metadata, response) { + Assert.equal(metadata.hasHeader("Authorization"), true); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("ETag", '"two"', false); + response.setHeader("Cache-control", "no-cache", false); + response.setHeader("Content-type", "text/plain", false); + var body = "Response body 2"; + response.bodyOutputStream.write(body, body.length); + clearCreds(); + }, + + // Test 3 + function (metadata, response) { + Assert.equal(metadata.hasHeader("Authorization"), false); + Assert.equal(metadata.getHeader("If-None-Match"), '"two"'); + response.setStatusLine(metadata.httpVersion, 401, "Authenticate"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + addCreds("http", "localhost"); + }, + function (metadata, response) { + Assert.equal(metadata.hasHeader("Authorization"), true); + Assert.equal(metadata.getHeader("If-None-Match"), '"two"'); + response.setStatusLine(metadata.httpVersion, 304, "OK"); + response.setHeader("ETag", '"two"', false); + clearCreds(); + }, + + // Test 4 + function (metadata, response) { + Assert.equal(metadata.hasHeader("Authorization"), false); + Assert.equal(metadata.getHeader("If-None-Match"), '"two"'); + response.setStatusLine(metadata.httpVersion, 407, "Proxy Authenticate"); + response.setHeader("Proxy-Authenticate", 'Basic realm="secret"', false); + addCreds("http", "localhost"); + }, + function (metadata, response) { + Assert.equal(metadata.hasHeader("Proxy-Authorization"), true); + Assert.equal(metadata.getHeader("If-None-Match"), '"two"'); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("ETag", '"three"', false); + response.setHeader("Cache-control", "no-cache", false); + response.setHeader("Content-type", "text/plain", false); + var body = "Response body 3"; + response.bodyOutputStream.write(body, body.length); + clearCreds(); + }, + + // Test 5 + function (metadata, response) { + Assert.equal(metadata.hasHeader("Proxy-Authorization"), false); + Assert.equal(metadata.getHeader("If-None-Match"), '"three"'); + response.setStatusLine(metadata.httpVersion, 407, "Proxy Authenticate"); + response.setHeader("Proxy-Authenticate", 'Basic realm="secret"', false); + addCreds("http", "localhost"); + }, + function (metadata, response) { + Assert.equal(metadata.hasHeader("Proxy-Authorization"), true); + Assert.equal(metadata.getHeader("If-None-Match"), '"three"'); + response.setStatusLine(metadata.httpVersion, 304, "OK"); + response.setHeader("ETag", '"three"', false); + response.setHeader("Cache-control", "no-cache", false); + clearCreds(); + }, +]; + +function handler(metadata, response) { + handlers.shift()(metadata, response); +} + +// Array of tests to run, self-driven + +function sync_and_run_next_test() { + syncWithCacheIOThread(function () { + tests.shift()(); + }); +} + +var tests = [ + // Test 1: 200 (cacheable) + function () { + var ch = makeChan(); + ch.asyncOpen( + new ChannelListener( + function (req, body) { + Assert.equal(body, "Response body 1"); + sync_and_run_next_test(); + }, + null, + CL_NOT_FROM_CACHE + ) + ); + }, + + // Test 2: 401 and 200 + new content + function () { + var ch = makeChan(); + ch.asyncOpen( + new ChannelListener( + function (req, body) { + Assert.equal(body, "Response body 2"); + sync_and_run_next_test(); + }, + null, + CL_NOT_FROM_CACHE + ) + ); + }, + + // Test 3: 401 and 304 + function () { + var ch = makeChan(); + ch.asyncOpen( + new ChannelListener( + function (req, body) { + Assert.equal(body, "Response body 2"); + sync_and_run_next_test(); + }, + null, + CL_FROM_CACHE + ) + ); + }, + + // Test 4: 407 and 200 + new content + function () { + var ch = makeChan(); + ch.asyncOpen( + new ChannelListener( + function (req, body) { + Assert.equal(body, "Response body 3"); + sync_and_run_next_test(); + }, + null, + CL_NOT_FROM_CACHE + ) + ); + }, + + // Test 5: 407 and 304 + function () { + var ch = makeChan(); + ch.asyncOpen( + new ChannelListener( + function (req, body) { + Assert.equal(body, "Response body 3"); + sync_and_run_next_test(); + }, + null, + CL_FROM_CACHE + ) + ); + }, + + // End of test run + function () { + httpserv.stop(do_test_finished); + }, +]; + +function run_test() { + do_get_profile(); + + httpserv = new HttpServer(); + httpserv.registerPathHandler("/", handler); + httpserv.start(-1); + + const prefs = Services.prefs; + prefs.setCharPref("network.proxy.http", "localhost"); + prefs.setIntPref("network.proxy.http_port", httpserv.identity.primaryPort); + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + prefs.setIntPref("network.proxy.type", 1); + prefs.setBoolPref("network.http.rcwn.enabled", false); + + tests.shift()(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug812167.js b/netwerk/test/unit/test_bug812167.js new file mode 100644 index 0000000000..8dc3c89e2b --- /dev/null +++ b/netwerk/test/unit/test_bug812167.js @@ -0,0 +1,141 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +/* +- get 302 with Cache-control: no-store +- check cache entry for the 302 response is cached only in memory device +- get 302 with Expires: -1 +- check cache entry for the 302 response is not cached at all +*/ + +var httpserver = null; +// Need to randomize, because apparently no one clears our cache +var randomPath1 = "/redirect-no-store/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI1", function () { + return "http://localhost:" + httpserver.identity.primaryPort + randomPath1; +}); + +var randomPath2 = "/redirect-expires-past/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI2", function () { + return "http://localhost:" + httpserver.identity.primaryPort + randomPath2; +}); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +var redirectHandler_NoStore_calls = 0; +function redirectHandler_NoStore(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader( + "Location", + "http://localhost:" + httpserver.identity.primaryPort + "/content", + false + ); + response.setHeader("Cache-control", "no-store"); + ++redirectHandler_NoStore_calls; +} + +var redirectHandler_ExpiresInPast_calls = 0; +function redirectHandler_ExpiresInPast(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader( + "Location", + "http://localhost:" + httpserver.identity.primaryPort + "/content", + false + ); + response.setHeader("Expires", "-1"); + ++redirectHandler_ExpiresInPast_calls; +} + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function check_response( + path, + request, + buffer, + expectedExpiration, + continuation +) { + Assert.equal(buffer, responseBody); + + // Entry is always there, old cache wrapping code does session->SetDoomEntriesIfExpired(false), + // just check it's not persisted or is expired (dep on the test). + asyncOpenCacheEntry( + path, + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + function (status, entry) { + Assert.equal(status, 0); + + // Expired entry is on disk, no-store entry is in memory + Assert.equal(entry.persistent, expectedExpiration); + + // Do the request again and check the server handler is called appropriately + var chan = make_channel(path); + chan.asyncOpen( + new ChannelListener(function (request, buffer) { + Assert.equal(buffer, responseBody); + + if (expectedExpiration) { + // Handler had to be called second time + Assert.equal(redirectHandler_ExpiresInPast_calls, 2); + } else { + // Handler had to be called second time (no-store forces validate), + // and we are just in memory + Assert.equal(redirectHandler_NoStore_calls, 2); + Assert.ok(!entry.persistent); + } + + continuation(); + }, null) + ); + } + ); +} + +function run_test_no_store() { + var chan = make_channel(randomURI1); + chan.asyncOpen( + new ChannelListener(function (request, buffer) { + // Cache-control: no-store response should only be found in the memory cache. + check_response(randomURI1, request, buffer, false, run_test_expires_past); + }, null) + ); +} + +function run_test_expires_past() { + var chan = make_channel(randomURI2); + chan.asyncOpen( + new ChannelListener(function (request, buffer) { + // Expires: -1 response should not be found in any cache. + check_response(randomURI2, request, buffer, true, finish_test); + }, null) + ); +} + +function finish_test() { + httpserver.stop(do_test_finished); +} + +function run_test() { + do_get_profile(); + + httpserver = new HttpServer(); + httpserver.registerPathHandler(randomPath1, redirectHandler_NoStore); + httpserver.registerPathHandler(randomPath2, redirectHandler_ExpiresInPast); + httpserver.registerPathHandler("/content", contentHandler); + httpserver.start(-1); + + run_test_no_store(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug826063.js b/netwerk/test/unit/test_bug826063.js new file mode 100644 index 0000000000..3e17df6461 --- /dev/null +++ b/netwerk/test/unit/test_bug826063.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that nsIPrivateBrowsingChannel.isChannelPrivate yields the correct + * result for various combinations of .setPrivate() and nsILoadContexts + */ + +"use strict"; + +var URIs = ["http://example.org", "https://example.org"]; + +function* getChannels() { + for (let u of URIs) { + yield NetUtil.newChannel({ + uri: u, + loadUsingSystemPrincipal: true, + }); + } +} + +function checkPrivate(channel, shouldBePrivate) { + Assert.equal( + channel.QueryInterface(Ci.nsIPrivateBrowsingChannel).isChannelPrivate, + shouldBePrivate + ); +} + +/** + * Default configuration + * Default is non-private + */ +add_test(function test_plain() { + for (let c of getChannels()) { + checkPrivate(c, false); + } + run_next_test(); +}); + +/** + * Explicitly setPrivate(true), no load context + */ +add_test(function test_setPrivate_private() { + for (let c of getChannels()) { + c.QueryInterface(Ci.nsIPrivateBrowsingChannel).setPrivate(true); + checkPrivate(c, true); + } + run_next_test(); +}); + +/** + * Explicitly setPrivate(false), no load context + */ +add_test(function test_setPrivate_regular() { + for (let c of getChannels()) { + c.QueryInterface(Ci.nsIPrivateBrowsingChannel).setPrivate(false); + checkPrivate(c, false); + } + run_next_test(); +}); + +/** + * Load context mandates private mode + */ +add_test(function test_LoadContextPrivate() { + let ctx = Cu.createPrivateLoadContext(); + for (let c of getChannels()) { + c.notificationCallbacks = ctx; + checkPrivate(c, true); + } + run_next_test(); +}); + +/** + * Load context mandates regular mode + */ +add_test(function test_LoadContextRegular() { + let ctx = Cu.createLoadContext(); + for (let c of getChannels()) { + c.notificationCallbacks = ctx; + checkPrivate(c, false); + } + run_next_test(); +}); + +// Do not test simultanous uses of .setPrivate and load context. +// There is little merit in doing so, and combining both will assert in +// Debug builds anyway. diff --git a/netwerk/test/unit/test_bug856978.js b/netwerk/test/unit/test_bug856978.js new file mode 100644 index 0000000000..57d64876dc --- /dev/null +++ b/netwerk/test/unit/test_bug856978.js @@ -0,0 +1,136 @@ +/* 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 test makes sure that the authorization header can get deleted e.g. by +// extensions if they are observing "http-on-modify-request". In a first step +// the auth cache is filled with credentials which then get added to the +// following request. On "http-on-modify-request" it is tested whether the +// authorization header got added at all and if so it gets removed. This test +// passes iff both succeeds. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var notification = "http-on-modify-request"; + +var httpServer = null; + +var authCredentials = "guest:guest"; +var authPath = "/authTest"; +var authCredsURL = "http://" + authCredentials + "@localhost:8888" + authPath; +var authURL = "http://localhost:8888" + authPath; + +function authHandler(metadata, response) { + if (metadata.hasHeader("Test")) { + // Lets see if the auth header got deleted. + var noAuthHeader = false; + if (!metadata.hasHeader("Authorization")) { + noAuthHeader = true; + } + Assert.ok(noAuthHeader); + } + // Not our test request yet. + else if (!metadata.hasHeader("Authorization")) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + } +} + +function RequestObserver() { + this.register(); +} + +RequestObserver.prototype = { + register() { + info("Registering " + notification); + Services.obs.addObserver(this, notification, true); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + + observe(subject, topic, data) { + if (topic == notification) { + if (!(subject instanceof Ci.nsIHttpChannel)) { + do_throw(notification + " observed a non-HTTP channel."); + } + try { + subject.getRequestHeader("Authorization"); + } catch (e) { + // Throw if there is no header to delete. We should get one iff caching + // the auth credentials is working and the header gets added _before_ + // "http-on-modify-request" gets called. + httpServer.stop(do_test_finished); + do_throw("No authorization header found, aborting!"); + } + // We are still here. Let's remove the authorization header now. + subject.setRequestHeader("Authorization", null, false); + } + }, +}; + +var listener = { + onStartRequest: function test_onStartR(request) {}, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + if (current_test < tests.length - 1) { + current_test++; + tests[current_test](); + } else { + do_test_pending(); + httpServer.stop(do_test_finished); + } + do_test_finished(); + }, +}; + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var tests = [startAuthHeaderTest, removeAuthHeaderTest]; + +var current_test = 0; + +// Must create a RequestObserver for the test to pass, we keep it in memory +// to avoid garbage collection. +// eslint-disable-next-line no-unused-vars +var requestObserver = null; + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(authPath, authHandler); + httpServer.start(8888); + + tests[0](); +} + +function startAuthHeaderTest() { + var chan = makeChan(authCredsURL); + chan.asyncOpen(listener); + + do_test_pending(); +} + +function removeAuthHeaderTest() { + // After caching the auth credentials in the first test, lets try to remove + // the authorization header now... + requestObserver = new RequestObserver(); + var chan = makeChan(authURL); + // Indicating that the request is coming from the second test. + chan.setRequestHeader("Test", "1", false); + chan.asyncOpen(listener); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug894586.js b/netwerk/test/unit/test_bug894586.js new file mode 100644 index 0000000000..bc25731d36 --- /dev/null +++ b/netwerk/test/unit/test_bug894586.js @@ -0,0 +1,135 @@ +/* + * Tests for bug 894586: nsSyncLoadService::PushSyncStreamToListener + * should not fail for channels of unknown size + */ + +"use strict"; + +var contentSecManager = Cc["@mozilla.org/contentsecuritymanager;1"].getService( + Ci.nsIContentSecurityManager +); + +function ProtocolHandler() { + this.uri = Cc["@mozilla.org/network/simple-uri-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec(this.scheme + ":dummy") + .finalize(); +} + +ProtocolHandler.prototype = { + /** nsIProtocolHandler */ + get scheme() { + return "x-bug894586"; + }, + newChannel(aURI, aLoadInfo) { + this.loadInfo = aLoadInfo; + return this; + }, + allowPort(port, scheme) { + return port != -1; + }, + + /** nsIChannel */ + get originalURI() { + return this.uri; + }, + get URI() { + return this.uri; + }, + owner: null, + notificationCallbacks: null, + get securityInfo() { + return null; + }, + get contentType() { + return "text/css"; + }, + set contentType(val) {}, + contentCharset: "UTF-8", + get contentLength() { + return -1; + }, + set contentLength(val) { + throw Components.Exception( + "Setting content length", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + }, + open() { + // throws an error if security checks fail + contentSecManager.performSecurityCheck(this, null); + + var file = do_get_file("test_bug894586.js", false); + Assert.ok(file.exists()); + var url = Services.io.newFileURI(file); + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).open(); + }, + asyncOpen(aListener, aContext) { + throw Components.Exception("Not implemented", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + contentDisposition: Ci.nsIChannel.DISPOSITION_INLINE, + get contentDispositionFilename() { + throw Components.Exception("No file name", Cr.NS_ERROR_NOT_AVAILABLE); + }, + get contentDispositionHeader() { + throw Components.Exception("No header", Cr.NS_ERROR_NOT_AVAILABLE); + }, + + /** nsIRequest */ + get name() { + return this.uri.spec; + }, + isPending: () => 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, + + /** nsISupports */ + QueryInterface: ChromeUtils.generateQI([ + "nsIProtocolHandler", + "nsIRequest", + "nsIChannel", + ]), +}; + +/** + * Attempt a sync load; we use the stylesheet service to do this for us, + * based on the knowledge that it forces a sync load under the hood. + */ +function run_test() { + var handler = new ProtocolHandler(); + + Services.io.registerProtocolHandler( + handler.scheme, + handler, + Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.URI_NOAUTH | + Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE | + Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE | + Ci.nsIProtocolHandler.URI_NON_PERSISTABLE | + Ci.nsIProtocolHandler.URI_SYNC_LOAD_IS_OK, + -1 + ); + try { + var ss = Cc["@mozilla.org/content/style-sheet-service;1"].getService( + Ci.nsIStyleSheetService + ); + ss.loadAndRegisterSheet(handler.uri, Ci.nsIStyleSheetService.AGENT_SHEET); + Assert.ok( + ss.sheetRegistered(handler.uri, Ci.nsIStyleSheetService.AGENT_SHEET) + ); + } finally { + Services.io.unregisterProtocolHandler(handler.scheme); + } +} + +// vim: set et ts=2 : diff --git a/netwerk/test/unit/test_bug935499.js b/netwerk/test/unit/test_bug935499.js new file mode 100644 index 0000000000..2da8168d2d --- /dev/null +++ b/netwerk/test/unit/test_bug935499.js @@ -0,0 +1,10 @@ +"use strict"; + +function run_test() { + var idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + + var isASCII = {}; + Assert.equal(idnService.convertToDisplayIDN("xn--", isASCII), "xn--"); +} diff --git a/netwerk/test/unit/test_cache-control_request.js b/netwerk/test/unit/test_cache-control_request.js new file mode 100644 index 0000000000..50d89e5cc0 --- /dev/null +++ b/netwerk/test/unit/test_cache-control_request.js @@ -0,0 +1,446 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +httpserver.start(-1); +var cache = null; + +var base_url = "http://localhost:" + httpserver.identity.primaryPort; +var resource_age_100 = "/resource_age_100"; +var resource_age_100_url = base_url + resource_age_100; +var resource_stale_100 = "/resource_stale_100"; +var resource_stale_100_url = base_url + resource_stale_100; +var resource_fresh_100 = "/resource_fresh_100"; +var resource_fresh_100_url = base_url + resource_fresh_100; + +// Test flags +var hit_server = false; + +function make_channel(url, cache_control) { + // Reset test global status + hit_server = false; + + var req = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); + req.QueryInterface(Ci.nsIHttpChannel); + if (cache_control) { + req.setRequestHeader("Cache-control", cache_control, false); + } + + return req; +} + +function make_uri(url) { + return Services.io.newURI(url); +} + +function resource_age_100_handler(metadata, response) { + hit_server = true; + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Age", "100", false); + response.setHeader("Last-Modified", date_string_from_now(-100), false); + response.setHeader("Expires", date_string_from_now(+9999), false); + + const body = "data1"; + response.bodyOutputStream.write(body, body.length); +} + +function resource_stale_100_handler(metadata, response) { + hit_server = true; + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Date", date_string_from_now(-200), false); + response.setHeader("Last-Modified", date_string_from_now(-200), false); + response.setHeader("Cache-Control", "max-age=100", false); + response.setHeader("Expires", date_string_from_now(-100), false); + + const body = "data2"; + response.bodyOutputStream.write(body, body.length); +} + +function resource_fresh_100_handler(metadata, response) { + hit_server = true; + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Last-Modified", date_string_from_now(0), false); + response.setHeader("Cache-Control", "max-age=100", false); + response.setHeader("Expires", date_string_from_now(+100), false); + + const body = "data3"; + response.bodyOutputStream.write(body, body.length); +} + +function run_test() { + do_get_profile(); + + do_test_pending(); + + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpserver.registerPathHandler(resource_age_100, resource_age_100_handler); + httpserver.registerPathHandler( + resource_stale_100, + resource_stale_100_handler + ); + httpserver.registerPathHandler( + resource_fresh_100, + resource_fresh_100_handler + ); + cache = getCacheStorage("disk"); + + wait_for_cache_index(run_next_test); +} + +// Here starts the list of tests + +// ============================================================================ +// Cache-Control: no-store + +add_test(() => { + // Must not create a cache entry + var ch = make_channel(resource_age_100_url, "no-store"); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(hit_server); + Assert.ok(!cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // Prepare state only, cache the entry + var ch = make_channel(resource_age_100_url); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // Check the prepared cache entry is used when no special directives are added + var ch = make_channel(resource_age_100_url); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // Try again, while we already keep a cache entry, + // the channel must not use it, entry should stay in the cache + var ch = make_channel(resource_age_100_url, "no-store"); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +// ============================================================================ +// Cache-Control: no-cache + +add_test(() => { + // Check the prepared cache entry is used when no special directives are added + var ch = make_channel(resource_age_100_url); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // The existing entry should be revalidated (we expect a server hit) + var ch = make_channel(resource_age_100_url, "no-cache"); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +// ============================================================================ +// Cache-Control: max-age + +add_test(() => { + // Check the prepared cache entry is used when no special directives are added + var ch = make_channel(resource_age_100_url); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // The existing entry's age is greater than the maximum requested, + // should hit server + var ch = make_channel(resource_age_100_url, "max-age=10"); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // The existing entry's age is greater than the maximum requested, + // but the max-stale directive says to use it when it's fresh enough + var ch = make_channel(resource_age_100_url, "max-age=10, max-stale=99999"); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // The existing entry's age is lesser than the maximum requested, + // should go from cache + var ch = make_channel(resource_age_100_url, "max-age=1000"); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +// ============================================================================ +// Cache-Control: max-stale + +add_test(() => { + // Preprate the entry first + var ch = make_channel(resource_stale_100_url); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_stale_100_url), "")); + + // Must shift the expiration time set on the entry to |now| be in the past + do_timeout(1500, run_next_test); + }, null) + ); +}); + +add_test(() => { + // Check it's not reused (as it's stale) when no special directives + // are provided + var ch = make_channel(resource_stale_100_url); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_stale_100_url), "")); + + do_timeout(1500, run_next_test); + }, null) + ); +}); + +add_test(() => { + // Accept cached responses of any stale time + var ch = make_channel(resource_stale_100_url, "max-stale"); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_stale_100_url), "")); + + do_timeout(1500, run_next_test); + }, null) + ); +}); + +add_test(() => { + // The entry is stale only by 100 seconds, accept it + var ch = make_channel(resource_stale_100_url, "max-stale=1000"); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_stale_100_url), "")); + + do_timeout(1500, run_next_test); + }, null) + ); +}); + +add_test(() => { + // The entry is stale by 100 seconds but we only accept a 10 seconds stale + // entry, go from server + var ch = make_channel(resource_stale_100_url, "max-stale=10"); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_stale_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +// ============================================================================ +// Cache-Control: min-fresh + +add_test(() => { + // Preprate the entry first + var ch = make_channel(resource_fresh_100_url); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_fresh_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // Check it's reused when no special directives are provided + var ch = make_channel(resource_fresh_100_url); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_fresh_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // Entry fresh enough to be served from the cache + var ch = make_channel(resource_fresh_100_url, "min-fresh=10"); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_fresh_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // The entry is not fresh enough + var ch = make_channel(resource_fresh_100_url, "min-fresh=1000"); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_fresh_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +// ============================================================================ +// Parser test, if the Cache-Control header would not parse correctly, the entry +// doesn't load from the server. + +add_test(() => { + var ch = make_channel( + resource_fresh_100_url, + 'unknown1,unknown2 = "a,b", min-fresh = 1000 ' + ); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_fresh_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + var ch = make_channel(resource_fresh_100_url, "no-cache = , min-fresh = 10"); + ch.asyncOpen( + new ChannelListener(function (request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_fresh_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +// ============================================================================ +// Done + +add_test(() => { + run_next_test(); + httpserver.stop(do_test_finished); +}); + +// ============================================================================ +// Helpers + +function date_string_from_now(delta_secs) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + d.setTime(d.getTime() + delta_secs * 1000); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + d.getUTCFullYear() + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_cache-entry-id.js b/netwerk/test/unit/test_cache-entry-id.js new file mode 100644 index 0000000000..f7df943506 --- /dev/null +++ b/netwerk/test/unit/test_cache-entry-id.js @@ -0,0 +1,218 @@ +/** + * Test for the "CacheEntryId" under several cases. + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +var httpServer = null; + +const responseContent = "response body"; +const responseContent2 = "response body 2"; +const altContent = "!@#$%^&*()"; +const altContentType = "text/binary"; + +function isParentProcess() { + let appInfo = Cc["@mozilla.org/xre/app-info;1"]; + return ( + !appInfo || + Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +var handlers = [ + (m, r) => { + r.bodyOutputStream.write(responseContent, responseContent.length); + }, + (m, r) => { + r.setStatusLine(m.httpVersion, 304, "Not Modified"); + }, + (m, r) => { + r.setStatusLine(m.httpVersion, 304, "Not Modified"); + }, + (m, r) => { + r.setStatusLine(m.httpVersion, 304, "Not Modified"); + }, + (m, r) => { + r.setStatusLine(m.httpVersion, 304, "Not Modified"); + }, + (m, r) => { + r.bodyOutputStream.write(responseContent2, responseContent2.length); + }, + (m, r) => { + r.setStatusLine(m.httpVersion, 304, "Not Modified"); + }, +]; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + + var handler = handlers.shift(); + if (handler) { + handler(metadata, response); + return; + } + + Assert.ok(false, "Should not reach here."); +} + +function fetch(preferredDataType = null) { + return new Promise(resolve => { + var chan = NetUtil.newChannel({ uri: URL, loadUsingSystemPrincipal: true }); + + if (preferredDataType) { + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + altContentType, + "", + Ci.nsICacheInfoChannel.ASYNC + ); + } + + chan.asyncOpen( + new ChannelListener((request, buffer, ctx, isFromCache, cacheEntryId) => { + resolve({ request, buffer, isFromCache, cacheEntryId }); + }, null) + ); + }); +} + +function check( + response, + content, + preferredDataType, + isFromCache, + cacheEntryIdChecker +) { + var cc = response.request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(response.buffer, content); + Assert.equal(cc.alternativeDataType, preferredDataType); + Assert.equal(response.isFromCache, isFromCache); + Assert.ok(!cacheEntryIdChecker || cacheEntryIdChecker(response.cacheEntryId)); + + return response; +} + +function writeAltData(request) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + var os = cc.openAlternativeOutputStream(altContentType, altContent.length); + os.write(altContent, altContent.length); + os.close(); + gc(); // We need to do a GC pass to ensure the cache entry has been freed. + + return new Promise(resolve => { + if (isParentProcess()) { + Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(resolve); + } else { + do_send_remote_message("flush"); + do_await_remote_message("flushed").then(resolve); + } + }); +} + +function run_test() { + do_get_profile(); + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + do_test_pending(); + + var targetCacheEntryId = null; + + return ( + Promise.resolve() + // Setup testing environment: Placing alternative data into HTTP cache. + .then(_ => fetch(altContentType)) + .then(r => + check( + r, + responseContent, + "", + false, + cacheEntryId => cacheEntryId === undefined + ) + ) + .then(r => writeAltData(r.request)) + + // Start testing. + .then(_ => fetch(altContentType)) + .then(r => + check( + r, + altContent, + altContentType, + true, + cacheEntryId => cacheEntryId !== undefined + ) + ) + .then(r => (targetCacheEntryId = r.cacheEntryId)) + + .then(_ => fetch()) + .then(r => + check( + r, + responseContent, + "", + true, + cacheEntryId => cacheEntryId === targetCacheEntryId + ) + ) + + .then(_ => fetch(altContentType)) + .then(r => + check( + r, + altContent, + altContentType, + true, + cacheEntryId => cacheEntryId === targetCacheEntryId + ) + ) + + .then(_ => fetch()) + .then(r => + check( + r, + responseContent, + "", + true, + cacheEntryId => cacheEntryId === targetCacheEntryId + ) + ) + + .then(_ => fetch()) // The response is changed here. + .then(r => + check( + r, + responseContent2, + "", + false, + cacheEntryId => cacheEntryId === undefined + ) + ) + + .then(_ => fetch()) + .then(r => + check( + r, + responseContent2, + "", + true, + cacheEntryId => + cacheEntryId !== undefined && cacheEntryId !== targetCacheEntryId + ) + ) + + // Tear down. + .catch(e => Assert.ok(false, "Unexpected exception: " + e)) + .then(_ => Assert.equal(handlers.length, 0)) + .then(_ => httpServer.stop(do_test_finished)) + ); +} diff --git a/netwerk/test/unit/test_cache2-00-service-get.js b/netwerk/test/unit/test_cache2-00-service-get.js new file mode 100644 index 0000000000..0d348a81de --- /dev/null +++ b/netwerk/test/unit/test_cache2-00-service-get.js @@ -0,0 +1,15 @@ +"use strict"; + +function run_test() { + // Just check the contract ID alias works well. + try { + var serviceA = Services.cache2; + Assert.ok(serviceA); + var serviceB = Services.cache2; + Assert.ok(serviceB); + + Assert.equal(serviceA, serviceB); + } catch (ex) { + do_throw("Cannot instantiate cache storage service: " + ex); + } +} diff --git a/netwerk/test/unit/test_cache2-01-basic.js b/netwerk/test/unit/test_cache2-01-basic.js new file mode 100644 index 0000000000..527c8a5ca9 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01-basic.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + // Open for rewrite (truncate), write different meta and data + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + null, + new OpenCallback(NEW, "a2m", "a2d", function (entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a2m", "a2d", function (entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-01a-basic-readonly.js b/netwerk/test/unit/test_cache2-01a-basic-readonly.js new file mode 100644 index 0000000000..99d250f7b5 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01a-basic-readonly.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://ro/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://ro/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + // Open for rewrite (truncate), write different meta and data + asyncOpenCacheEntry( + "http://ro/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + null, + new OpenCallback(NEW, "a2m", "a2d", function (entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://ro/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + new OpenCallback(NORMAL, "a2m", "a2d", function (entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-01b-basic-datasize.js b/netwerk/test/unit/test_cache2-01b-basic-datasize.js new file mode 100644 index 0000000000..f7b090958f --- /dev/null +++ b/netwerk/test/unit/test_cache2-01b-basic-datasize.js @@ -0,0 +1,51 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | WAITFORWRITE, "a1m", "a1d", function (entry) { + // Open for read and check + Assert.equal(entry.dataSize, 3); + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + // Open for rewrite (truncate), write different meta and data + Assert.equal(entry.dataSize, 3); + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + null, + new OpenCallback(NEW | WAITFORWRITE, "a2m", "a2d", function ( + entry + ) { + // Open for read and check + Assert.equal(entry.dataSize, 3); + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a2m", "a2d", function (entry) { + Assert.equal(entry.dataSize, 3); + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-01c-basic-hasmeta-only.js b/netwerk/test/unit/test_cache2-01c-basic-hasmeta-only.js new file mode 100644 index 0000000000..7a6dcceb2b --- /dev/null +++ b/netwerk/test/unit/test_cache2-01c-basic-hasmeta-only.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://mt/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | METAONLY, "a1m", "a1d", function (entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://mt/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "", function (entry) { + // Open for rewrite (truncate), write different meta and data + asyncOpenCacheEntry( + "http://mt/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + null, + new OpenCallback(NEW, "a2m", "a2d", function (entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://mt/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a2m", "a2d", function (entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-01d-basic-not-wanted.js b/netwerk/test/unit/test_cache2-01d-basic-not-wanted.js new file mode 100644 index 0000000000..a9dc5ef93c --- /dev/null +++ b/netwerk/test/unit/test_cache2-01d-basic-not-wanted.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + // Open but don't want the entry + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NOTWANTED, "a1m", "a1d", function (entry) { + // Open for read again and check the entry is OK + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-01e-basic-bypass-if-busy.js b/netwerk/test/unit/test_cache2-01e-basic-bypass-if-busy.js new file mode 100644 index 0000000000..dd6cdf3d5a --- /dev/null +++ b/netwerk/test/unit/test_cache2-01e-basic-bypass-if-busy.js @@ -0,0 +1,39 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, delay the actual write + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | DONTFILL, "a1m", "a1d", function (entry) { + var bypassed = false; + + // Open and bypass + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_BYPASS_IF_BUSY, + null, + new OpenCallback(NOTFOUND, "", "", function (entry) { + Assert.ok(!bypassed); + bypassed = true; + }) + ); + + // do_execute_soon for two reasons: + // 1. we want finish_cache2_test call for sure after do_test_pending, but all the callbacks here + // may invoke synchronously + // 2. precaution when the OPEN_BYPASS_IF_BUSY invocation become a post one day + executeSoon(function () { + Assert.ok(bypassed); + finish_cache2_test(); + }); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-01f-basic-openTruncate.js b/netwerk/test/unit/test_cache2-01f-basic-openTruncate.js new file mode 100644 index 0000000000..9d514f7cc7 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01f-basic-openTruncate.js @@ -0,0 +1,24 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var storage = getCacheStorage("disk"); + var entry = storage.openTruncate(createURI("http://new1/"), ""); + Assert.ok(!!entry); + + // Fill the entry, and when done, check it's content + new OpenCallback(NEW, "meta", "data", function () { + asyncOpenCacheEntry( + "http://new1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "meta", "data", function () { + finish_cache2_test(); + }) + ); + }).onCacheEntryAvailable(entry, true, 0); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-02-open-non-existing.js b/netwerk/test/unit/test_cache2-02-open-non-existing.js new file mode 100644 index 0000000000..c824bf33a3 --- /dev/null +++ b/netwerk/test/unit/test_cache2-02-open-non-existing.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open non-existing for read, should fail + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + new OpenCallback(NOTFOUND, null, null, function (entry) { + // Open the same non-existing for read again, should fail second time + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + new OpenCallback(NOTFOUND, null, null, function (entry) { + // Try it again normally, should go + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "b1m", "b1d", function (entry) { + // ...and check + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "b1m", "b1d", function (entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-02b-open-non-existing-and-doom.js b/netwerk/test/unit/test_cache2-02b-open-non-existing-and-doom.js new file mode 100644 index 0000000000..c4b2a679f2 --- /dev/null +++ b/netwerk/test/unit/test_cache2-02b-open-non-existing-and-doom.js @@ -0,0 +1,180 @@ +"use strict"; + +add_task(async function test() { + do_get_profile(); + do_test_pending(); + + await new Promise(resolve => { + // Open non-existing for read, should fail + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + new OpenCallback(NOTFOUND, null, null, function (entry) { + resolve(entry); + }) + ); + }); + + await new Promise(resolve => { + // Open the same non-existing for read again, should fail second time + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + new OpenCallback(NOTFOUND, null, null, function (entry) { + resolve(entry); + }) + ); + }); + + await new Promise(resolve => { + // Try it again normally, should go + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "b1m", "b1d", function (entry) { + resolve(entry); + }) + ); + }); + + await new Promise(resolve => { + // ...and check + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "b1m", "b1d", function (entry) { + resolve(entry); + }) + ); + }); + + Services.prefs.setBoolPref("network.cache.bug1708673", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.cache.bug1708673"); + }); + + let asyncDoomVisitor = new Promise(resolve => { + let doomTasks = []; + let visitor = { + onCacheStorageInfo() {}, + async onCacheEntryInfo( + aURI, + aIdEnhance, + aDataSize, + aAltDataSize, + aFetchCount, + aLastModifiedTime, + aExpirationTime, + aPinned, + aInfo + ) { + doomTasks.push( + new Promise(resolve => { + Services.cache2 + .diskCacheStorage(aInfo, false) + .asyncDoomURI(aURI, aIdEnhance, { + onCacheEntryDoomed() { + info("doomed"); + resolve(); + }, + }); + }) + ); + }, + onCacheEntryVisitCompleted() { + Promise.allSettled(doomTasks).then(resolve); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + Services.cache2.asyncVisitAllStorages(visitor, true); + }); + + let asyncOpenVisitor = new Promise(resolve => { + let openTasks = []; + let visitor = { + onCacheStorageInfo() {}, + async onCacheEntryInfo( + aURI, + aIdEnhance, + aDataSize, + aAltDataSize, + aFetchCount, + aLastModifiedTime, + aExpirationTime, + aPinned, + aInfo + ) { + info(`found ${aURI.spec}`); + openTasks.push( + new Promise(r2 => { + Services.cache2 + .diskCacheStorage(aInfo, false) + .asyncOpenURI( + aURI, + "", + Ci.nsICacheStorage.OPEN_READONLY | + Ci.nsICacheStorage.OPEN_SECRETLY, + { + onCacheEntryCheck() { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + onCacheEntryAvailable(entry, isnew, status) { + info("opened"); + r2(); + }, + QueryInterface: ChromeUtils.generateQI([ + "nsICacheEntryOpenCallback", + ]), + } + ); + }) + ); + }, + onCacheEntryVisitCompleted() { + Promise.all(openTasks).then(resolve); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + Services.cache2.asyncVisitAllStorages(visitor, true); + }); + + await Promise.all([asyncDoomVisitor, asyncOpenVisitor]); + + info("finished visiting"); + + await new Promise(resolve => { + let entryCount = 0; + let visitor = { + onCacheStorageInfo() {}, + async onCacheEntryInfo( + aURI, + aIdEnhance, + aDataSize, + aAltDataSize, + aFetchCount, + aLastModifiedTime, + aExpirationTime, + aPinned, + aInfo + ) { + entryCount++; + }, + onCacheEntryVisitCompleted() { + Assert.equal(entryCount, 0); + resolve(); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + Services.cache2.asyncVisitAllStorages(visitor, true); + }); + + finish_cache2_test(); +}); diff --git a/netwerk/test/unit/test_cache2-03-oncacheentryavail-throws.js b/netwerk/test/unit/test_cache2-03-oncacheentryavail-throws.js new file mode 100644 index 0000000000..92753dc7f3 --- /dev/null +++ b/netwerk/test/unit/test_cache2-03-oncacheentryavail-throws.js @@ -0,0 +1,36 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open but let OCEA throw + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | THROWAVAIL, null, null, function (entry) { + // Try it again, should go + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "c1m", "c1d", function (entry) { + // ...and check + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(false, "c1m", "c1d", function (entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-04-oncacheentryavail-throws2x.js b/netwerk/test/unit/test_cache2-04-oncacheentryavail-throws2x.js new file mode 100644 index 0000000000..dce2b483ff --- /dev/null +++ b/netwerk/test/unit/test_cache2-04-oncacheentryavail-throws2x.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open but let OCEA throw + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | THROWAVAIL, null, null, function (entry) { + // Open but let OCEA throw ones again + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | THROWAVAIL, null, null, function (entry) { + // Try it again, should go + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "d1m", "d1d", function (entry) { + // ...and check + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "d1m", "d1d", function (entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-05-visit.js b/netwerk/test/unit/test_cache2-05-visit.js new file mode 100644 index 0000000000..7ed9b9effd --- /dev/null +++ b/netwerk/test/unit/test_cache2-05-visit.js @@ -0,0 +1,113 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var storage = getCacheStorage("disk"); + var mc = new MultipleCallbacks(4, function () { + // Method asyncVisitStorage() gets the data from index on Cache I/O thread + // with INDEX priority, so it is ensured that index contains information + // about all pending writes. However, OpenCallback emulates network latency + // by postponing the writes using do_execute_soon. We must do the same here + // to make sure that all writes are posted to Cache I/O thread before we + // visit the storage. + executeSoon(function () { + syncWithCacheIOThread(function () { + var expectedConsumption = 4096; + + storage.asyncVisitStorage( + // Test should store 4 entries + new VisitCallback( + 4, + expectedConsumption, + ["http://a/", "http://b/", "http://c/", "http://d/"], + function () { + storage.asyncVisitStorage( + // Still 4 entries expected, now don't walk them + new VisitCallback(4, expectedConsumption, null, function () { + finish_cache2_test(); + }), + false + ); + } + ), + true + ); + }); + }); + }); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "b1m", "b1d", function (entry) { + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "b1m", "b1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "c1m", "c1d", function (entry) { + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "c1m", "c1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "d1m", "d1d", function (entry) { + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "d1m", "d1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-06-pb-mode.js b/netwerk/test/unit/test_cache2-06-pb-mode.js new file mode 100644 index 0000000000..376eba22a2 --- /dev/null +++ b/netwerk/test/unit/test_cache2-06-pb-mode.js @@ -0,0 +1,50 @@ +"use strict"; + +function exitPB() { + Services.obs.notifyObservers(null, "last-pb-context-exited"); +} + +function run_test() { + do_get_profile(); + + // Store PB entry + asyncOpenCacheEntry( + "http://p1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.private, + new OpenCallback(NEW, "p1m", "p1d", function (entry) { + asyncOpenCacheEntry( + "http://p1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.private, + new OpenCallback(NORMAL, "p1m", "p1d", function (entry) { + // Check it's there + syncWithCacheIOThread(function () { + var storage = getCacheStorage( + "disk", + Services.loadContextInfo.private + ); + storage.asyncVisitStorage( + new VisitCallback(1, 12, ["http://p1/"], function () { + // Simulate PB exit + exitPB(); + // Check the entry is gone + storage.asyncVisitStorage( + new VisitCallback(0, 0, [], function () { + finish_cache2_test(); + }), + true + ); + }), + true + ); + }); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-07-visit-memory.js b/netwerk/test/unit/test_cache2-07-visit-memory.js new file mode 100644 index 0000000000..c3c56ee4cc --- /dev/null +++ b/netwerk/test/unit/test_cache2-07-visit-memory.js @@ -0,0 +1,123 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Add entry to the memory storage + var mc = new MultipleCallbacks(5, function () { + // Check it's there by visiting the storage + syncWithCacheIOThread(function () { + var storage = getCacheStorage("memory"); + storage.asyncVisitStorage( + new VisitCallback(1, 12, ["http://mem1/"], function () { + storage = getCacheStorage("disk"); + storage.asyncVisitStorage( + // Previous tests should store 4 disk entries + new VisitCallback( + 4, + 4096, + ["http://a/", "http://b/", "http://c/", "http://d/"], + function () { + finish_cache2_test(); + } + ), + true + ); + }), + true + ); + }); + }); + + asyncOpenCacheEntry( + "http://mem1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "m1m", "m1d", function (entry) { + asyncOpenCacheEntry( + "http://mem1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "m1m", "m1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-07a-open-memory.js b/netwerk/test/unit/test_cache2-07a-open-memory.js new file mode 100644 index 0000000000..3392ee33fc --- /dev/null +++ b/netwerk/test/unit/test_cache2-07a-open-memory.js @@ -0,0 +1,81 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // First check how behaves the memory storage. + + asyncOpenCacheEntry( + "http://mem-first/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "mem1-meta", "mem1-data", function (entryM1) { + Assert.ok(!entryM1.persistent); + asyncOpenCacheEntry( + "http://mem-first/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "mem1-meta", "mem1-data", function (entryM2) { + Assert.ok(!entryM1.persistent); + Assert.ok(!entryM2.persistent); + + // Now check the disk storage behavior. + + asyncOpenCacheEntry( + "http://disk-first/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + // Must wait for write, since opening the entry as memory-only before the disk one + // is written would cause NS_ERROR_NOT_AVAILABLE from openOutputStream when writing + // this disk entry since it's doomed during opening of the memory-only entry for the same URL. + new OpenCallback( + NEW | WAITFORWRITE, + "disk1-meta", + "disk1-data", + function (entryD1) { + Assert.ok(entryD1.persistent); + // Now open the same URL as a memory-only entry, the disk entry must be doomed. + asyncOpenCacheEntry( + "http://disk-first/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + // This must be recreated + new OpenCallback(NEW, "mem2-meta", "mem2-data", function ( + entryD2 + ) { + Assert.ok(entryD1.persistent); + Assert.ok(!entryD2.persistent); + // Check we get it back, even when opening via the disk storage + asyncOpenCacheEntry( + "http://disk-first/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback( + NORMAL, + "mem2-meta", + "mem2-data", + function (entryD3) { + Assert.ok(entryD1.persistent); + Assert.ok(!entryD2.persistent); + Assert.ok(!entryD3.persistent); + finish_cache2_test(); + } + ) + ); + }) + ); + } + ) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-08-evict-disk-by-memory-storage.js b/netwerk/test/unit/test_cache2-08-evict-disk-by-memory-storage.js new file mode 100644 index 0000000000..395c41dd7c --- /dev/null +++ b/netwerk/test/unit/test_cache2-08-evict-disk-by-memory-storage.js @@ -0,0 +1,25 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + var storage = getCacheStorage("memory"); + // Have to fail + storage.asyncDoomURI( + createURI("http://a/"), + "", + new EvictionCallback(false, function () { + finish_cache2_test(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-09-evict-disk-by-uri.js b/netwerk/test/unit/test_cache2-09-evict-disk-by-uri.js new file mode 100644 index 0000000000..ea2a4ae775 --- /dev/null +++ b/netwerk/test/unit/test_cache2-09-evict-disk-by-uri.js @@ -0,0 +1,32 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + var storage = getCacheStorage("disk"); + storage.asyncDoomURI( + createURI("http://a/"), + "", + new EvictionCallback(true, function () { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-10-evict-direct.js b/netwerk/test/unit/test_cache2-10-evict-direct.js new file mode 100644 index 0000000000..1e3689e904 --- /dev/null +++ b/netwerk/test/unit/test_cache2-10-evict-direct.js @@ -0,0 +1,29 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "b1m", "b1d", function (entry) { + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "b1m", "b1d", function (entry) { + entry.asyncDoom( + new EvictionCallback(true, function () { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-10b-evict-direct-immediate.js b/netwerk/test/unit/test_cache2-10b-evict-direct-immediate.js new file mode 100644 index 0000000000..83728af20d --- /dev/null +++ b/netwerk/test/unit/test_cache2-10b-evict-direct-immediate.js @@ -0,0 +1,21 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | DOOMED, "b1m", "b1d", function (entry) { + entry.asyncDoom( + new EvictionCallback(true, function () { + finish_cache2_test(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-11-evict-memory.js b/netwerk/test/unit/test_cache2-11-evict-memory.js new file mode 100644 index 0000000000..ee4790c661 --- /dev/null +++ b/netwerk/test/unit/test_cache2-11-evict-memory.js @@ -0,0 +1,89 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var storage = getCacheStorage("memory"); + var mc = new MultipleCallbacks(3, function () { + storage.asyncEvictStorage( + new EvictionCallback(true, function () { + storage.asyncVisitStorage( + new VisitCallback(0, 0, [], function () { + var storage = getCacheStorage("disk"); + + var expectedConsumption = 2048; + + storage.asyncVisitStorage( + new VisitCallback( + 2, + expectedConsumption, + ["http://a/", "http://b/"], + function () { + finish_cache2_test(); + } + ), + true + ); + }), + true + ); + }) + ); + }); + + asyncOpenCacheEntry( + "http://mem1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "m2m", "m2d", function (entry) { + asyncOpenCacheEntry( + "http://mem1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "m2m", "m2d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-12-evict-disk.js b/netwerk/test/unit/test_cache2-12-evict-disk.js new file mode 100644 index 0000000000..9fb9a94416 --- /dev/null +++ b/netwerk/test/unit/test_cache2-12-evict-disk.js @@ -0,0 +1,81 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var mc = new MultipleCallbacks(3, function () { + var storage = getCacheStorage("disk"); + storage.asyncEvictStorage( + new EvictionCallback(true, function () { + storage.asyncVisitStorage( + new VisitCallback(0, 0, [], function () { + var storage = getCacheStorage("memory"); + storage.asyncVisitStorage( + new VisitCallback(0, 0, [], function () { + finish_cache2_test(); + }), + true + ); + }), + true + ); + }) + ); + }); + + asyncOpenCacheEntry( + "http://mem1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "m2m", "m2d", function (entry) { + asyncOpenCacheEntry( + "http://mem1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "m2m", "m2d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "b1m", "b1d", function (entry) { + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "b1m", "b1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-13-evict-non-existing.js b/netwerk/test/unit/test_cache2-13-evict-non-existing.js new file mode 100644 index 0000000000..a2d40fd153 --- /dev/null +++ b/netwerk/test/unit/test_cache2-13-evict-non-existing.js @@ -0,0 +1,16 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var storage = getCacheStorage("disk"); + storage.asyncDoomURI( + createURI("http://non-existing/"), + "", + new EvictionCallback(false, function () { + finish_cache2_test(); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-14-concurent-readers.js b/netwerk/test/unit/test_cache2-14-concurent-readers.js new file mode 100644 index 0000000000..d2e80582dc --- /dev/null +++ b/netwerk/test/unit/test_cache2-14-concurent-readers.js @@ -0,0 +1,48 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "x1m", "x1d", function (entry) { + // nothing to do here, we expect concurent callbacks to get + // all notified, then the test finishes + }) + ); + + var mc = new MultipleCallbacks(3, finish_cache2_test); + + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "x1m", "x1d", function (entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "x1m", "x1d", function (entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "x1m", "x1d", function (entry) { + mc.fired(); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-14b-concurent-readers-complete.js b/netwerk/test/unit/test_cache2-14b-concurent-readers-complete.js new file mode 100644 index 0000000000..244dca9a3b --- /dev/null +++ b/netwerk/test/unit/test_cache2-14b-concurent-readers-complete.js @@ -0,0 +1,76 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "x1m", "x1d", function (entry) { + // nothing to do here, we expect concurent callbacks to get + // all notified, then the test finishes + }) + ); + + var mc = new MultipleCallbacks(3, finish_cache2_test); + + var order = 0; + + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback( + NORMAL | COMPLETE | NOTIFYBEFOREREAD, + "x1m", + "x1d", + function (entry, beforeReading) { + if (beforeReading) { + ++order; + Assert.equal(order, 3); + } else { + mc.fired(); + } + } + ) + ); + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL | NOTIFYBEFOREREAD, "x1m", "x1d", function ( + entry, + beforeReading + ) { + if (beforeReading) { + ++order; + Assert.equal(order, 1); + } else { + mc.fired(); + } + }) + ); + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL | NOTIFYBEFOREREAD, "x1m", "x1d", function ( + entry, + beforeReading + ) { + if (beforeReading) { + ++order; + Assert.equal(order, 2); + } else { + mc.fired(); + } + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-15-conditional-304.js b/netwerk/test/unit/test_cache2-15-conditional-304.js new file mode 100644 index 0000000000..2964150628 --- /dev/null +++ b/netwerk/test/unit/test_cache2-15-conditional-304.js @@ -0,0 +1,60 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://304/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "31m", "31d", function (entry) { + // Open normally but wait for validation from the server + asyncOpenCacheEntry( + "http://304/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(REVAL, "31m", "31d", function (entry) { + // emulate 304 from the server + executeSoon(function () { + entry.setValid(); // this will trigger OpenCallbacks bellow + }); + }) + ); + + var mc = new MultipleCallbacks(3, finish_cache2_test); + + asyncOpenCacheEntry( + "http://304/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "31m", "31d", function (entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://304/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "31m", "31d", function (entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://304/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "31m", "31d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-16-conditional-200.js b/netwerk/test/unit/test_cache2-16-conditional-200.js new file mode 100644 index 0000000000..5dd4af7ae8 --- /dev/null +++ b/netwerk/test/unit/test_cache2-16-conditional-200.js @@ -0,0 +1,76 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "21m", "21d", function (entry) { + asyncOpenCacheEntry( + "http://200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "21m", "21d", function (entry) { + // Open normally but wait for validation from the server + asyncOpenCacheEntry( + "http://200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(REVAL, "21m", "21d", function (entry) { + // emulate 200 from server (new content) + executeSoon(function () { + var entry2 = entry.recreate(); + + // now fill the new entry, use OpenCallback directly for it + new OpenCallback( + NEW, + "22m", + "22d", + function () {} + ).onCacheEntryAvailable(entry2, true, Cr.NS_OK); + }); + }) + ); + + var mc = new MultipleCallbacks(3, finish_cache2_test); + + asyncOpenCacheEntry( + "http://200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "22m", "22d", function (entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "22m", "22d", function (entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "22m", "22d", function (entry) { + mc.fired(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-17-evict-all.js b/netwerk/test/unit/test_cache2-17-evict-all.js new file mode 100644 index 0000000000..83829c631e --- /dev/null +++ b/netwerk/test/unit/test_cache2-17-evict-all.js @@ -0,0 +1,17 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + Services.cache2.clear(); + + var storage = getCacheStorage("disk"); + storage.asyncVisitStorage( + new VisitCallback(0, 0, [], function () { + finish_cache2_test(); + }), + true + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-18-not-valid.js b/netwerk/test/unit/test_cache2-18-not-valid.js new file mode 100644 index 0000000000..64459557e0 --- /dev/null +++ b/netwerk/test/unit/test_cache2-18-not-valid.js @@ -0,0 +1,38 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write but expect it to fail, since other callback will recreate (and doom) + // the first entry before it opens output stream (note: in case of problems the DOOMED flag + // can be removed, it is not the test failure when opening the output stream on recreated entry. + asyncOpenCacheEntry( + "http://nv/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | DOOMED, "v1m", "v1d", function (entry) { + // Open for rewrite (don't validate), write different meta and data + asyncOpenCacheEntry( + "http://nv/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NOTVALID | RECREATE, "v2m", "v2d", function (entry) { + // And check... + asyncOpenCacheEntry( + "http://nv/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "v2m", "v2d", function (entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-19-range-206.js b/netwerk/test/unit/test_cache2-19-range-206.js new file mode 100644 index 0000000000..ece438b00e --- /dev/null +++ b/netwerk/test/unit/test_cache2-19-range-206.js @@ -0,0 +1,65 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://r206/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "206m", "206part1-", function (entry) { + // Open normally but wait for validation from the server + asyncOpenCacheEntry( + "http://r206/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(PARTIAL, "206m", "206part1-", function (entry) { + // emulate 206 from the server, i.e. resume transaction and write content to the output stream + new OpenCallback( + NEW | WAITFORWRITE | PARTIAL, + "206m", + "-part2", + function (entry) { + entry.setValid(); + } + ).onCacheEntryAvailable(entry, true, Cr.NS_OK); + }) + ); + + var mc = new MultipleCallbacks(3, finish_cache2_test); + + asyncOpenCacheEntry( + "http://r206/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "206m", "206part1--part2", function (entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://r206/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "206m", "206part1--part2", function (entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://r206/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "206m", "206part1--part2", function (entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-20-range-200.js b/netwerk/test/unit/test_cache2-20-range-200.js new file mode 100644 index 0000000000..0bd929a675 --- /dev/null +++ b/netwerk/test/unit/test_cache2-20-range-200.js @@ -0,0 +1,72 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://r200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "200m1", "200part1a-", function (entry) { + // Open normally but wait for validation from the server + asyncOpenCacheEntry( + "http://r200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(PARTIAL, "200m1", "200part1a-", function (entry) { + // emulate 200 from the server, i.e. recreate the entry, resume transaction and + // write new content to the output stream + new OpenCallback( + NEW | WAITFORWRITE | RECREATE, + "200m2", + "200part1b--part2b", + function (entry) { + entry.setValid(); + } + ).onCacheEntryAvailable(entry, true, Cr.NS_OK); + }) + ); + + var mc = new MultipleCallbacks(3, finish_cache2_test); + + asyncOpenCacheEntry( + "http://r200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "200m2", "200part1b--part2b", function ( + entry + ) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://r200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "200m2", "200part1b--part2b", function ( + entry + ) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://r200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "200m2", "200part1b--part2b", function ( + entry + ) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-21-anon-storage.js b/netwerk/test/unit/test_cache2-21-anon-storage.js new file mode 100644 index 0000000000..c84ca0ff77 --- /dev/null +++ b/netwerk/test/unit/test_cache2-21-anon-storage.js @@ -0,0 +1,52 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Create and check an entry anon disk storage + asyncOpenCacheEntry( + "http://anon1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.anonymous, + new OpenCallback(NEW, "an1", "an1", function (entry) { + asyncOpenCacheEntry( + "http://anon1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.anonymous, + new OpenCallback(NORMAL, "an1", "an1", function (entry) { + // Create and check an entry non-anon disk storage + asyncOpenCacheEntry( + "http://anon1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NEW, "na1", "na1", function (entry) { + asyncOpenCacheEntry( + "http://anon1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NORMAL, "na1", "na1", function (entry) { + // check the anon entry is still there and intact + asyncOpenCacheEntry( + "http://anon1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.anonymous, + new OpenCallback(NORMAL, "an1", "an1", function (entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-22-anon-visit.js b/netwerk/test/unit/test_cache2-22-anon-visit.js new file mode 100644 index 0000000000..1768f142bd --- /dev/null +++ b/netwerk/test/unit/test_cache2-22-anon-visit.js @@ -0,0 +1,43 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var mc = new MultipleCallbacks(2, function () { + var storage = getCacheStorage("disk", Services.loadContextInfo.default); + storage.asyncVisitStorage( + new VisitCallback(1, 1024, ["http://an2/"], function () { + storage = getCacheStorage("disk", Services.loadContextInfo.anonymous); + storage.asyncVisitStorage( + new VisitCallback(1, 1024, ["http://an2/"], function () { + finish_cache2_test(); + }), + true + ); + }), + true + ); + }); + + asyncOpenCacheEntry( + "http://an2/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NEW | WAITFORWRITE, "an2", "an2", function (entry) { + mc.fired(); + }) + ); + + asyncOpenCacheEntry( + "http://an2/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.anonymous, + new OpenCallback(NEW | WAITFORWRITE, "an2", "an2", function (entry) { + mc.fired(); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-23-read-over-chunk.js b/netwerk/test/unit/test_cache2-23-read-over-chunk.js new file mode 100644 index 0000000000..89bdb2d963 --- /dev/null +++ b/netwerk/test/unit/test_cache2-23-read-over-chunk.js @@ -0,0 +1,34 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + const kChunkSize = 256 * 1024; + + var payload = ""; + for (var i = 0; i < kChunkSize + 10; ++i) { + if (i < kChunkSize - 5) { + payload += "0"; + } else { + payload += String.fromCharCode(i + 65); + } + } + + asyncOpenCacheEntry( + "http://read/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + Services.loadContextInfo.default, + new OpenCallback(NEW | WAITFORWRITE, "", payload, function (entry) { + var is = entry.openInputStream(0); + pumpReadStream(is, function (read) { + Assert.equal(read.length, kChunkSize + 10); + is.close(); + Assert.ok(read == payload); // not using do_check_eq since logger will fail for the 1/4MB string + finish_cache2_test(); + }); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-24-exists.js b/netwerk/test/unit/test_cache2-24-exists.js new file mode 100644 index 0000000000..7f7c50e9f0 --- /dev/null +++ b/netwerk/test/unit/test_cache2-24-exists.js @@ -0,0 +1,43 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var mc = new MultipleCallbacks(2, function () { + var mem = getCacheStorage("memory"); + var disk = getCacheStorage("disk"); + + Assert.ok(disk.exists(createURI("http://m1/"), "")); + Assert.ok(mem.exists(createURI("http://m1/"), "")); + Assert.ok(!mem.exists(createURI("http://m2/"), "")); + Assert.ok(disk.exists(createURI("http://d1/"), "")); + do_check_throws_nsIException( + () => disk.exists(createURI("http://d2/"), ""), + "NS_ERROR_NOT_AVAILABLE" + ); + + finish_cache2_test(); + }); + + asyncOpenCacheEntry( + "http://d1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NEW | WAITFORWRITE, "meta", "data", function (entry) { + mc.fired(); + }) + ); + + asyncOpenCacheEntry( + "http://m1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NEW | WAITFORWRITE, "meta", "data", function (entry) { + mc.fired(); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-25-chunk-memory-limit.js b/netwerk/test/unit/test_cache2-25-chunk-memory-limit.js new file mode 100644 index 0000000000..cbad8918ad --- /dev/null +++ b/netwerk/test/unit/test_cache2-25-chunk-memory-limit.js @@ -0,0 +1,53 @@ +"use strict"; + +function gen_200k() { + var i; + var data = "0123456789ABCDEFGHIJLKMNO"; + for (i = 0; i < 13; i++) { + data += data; + } + return data; +} + +// Keep the output stream of the first entry in a global variable, so the +// CacheFile and its buffer isn't released before we write the data to the +// second entry. +var oStr; + +function run_test() { + do_get_profile(); + + // set max chunks memory so that only one full chunk fits within the limit + Services.prefs.setIntPref("browser.cache.disk.max_chunks_memory_usage", 300); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function (status, entry) { + Assert.equal(status, Cr.NS_OK); + var data = gen_200k(); + oStr = entry.openOutputStream(0, data.length); + Assert.equal(data.length, oStr.write(data, data.length)); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function (status, entry) { + Assert.equal(status, Cr.NS_OK); + var oStr2 = entry.openOutputStream(0, data.length); + do_check_throws_nsIException( + () => oStr2.write(data, data.length), + "NS_ERROR_OUT_OF_MEMORY" + ); + finish_cache2_test(); + } + ); + } + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-26-no-outputstream-open.js b/netwerk/test/unit/test_cache2-26-no-outputstream-open.js new file mode 100644 index 0000000000..dbcd699d67 --- /dev/null +++ b/netwerk/test/unit/test_cache2-26-no-outputstream-open.js @@ -0,0 +1,36 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, but never write and never mark valid + asyncOpenCacheEntry( + "http://no-data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback( + NEW | METAONLY | DONTSETVALID | WAITFORWRITE, + "meta", + "", + function (entry) { + // Open again, we must get the callback and zero-length data + executeSoon(() => { + Cu.forceGC(); // invokes OnHandleClosed on the entry + + asyncOpenCacheEntry( + "http://no-data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "meta", "", function (entry) { + finish_cache2_test(); + }) + ); + }); + } + ) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-27-force-valid-for.js b/netwerk/test/unit/test_cache2-27-force-valid-for.js new file mode 100644 index 0000000000..78ac7eb847 --- /dev/null +++ b/netwerk/test/unit/test_cache2-27-force-valid-for.js @@ -0,0 +1,35 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var mc = new MultipleCallbacks(2, function () { + finish_cache2_test(); + }); + + asyncOpenCacheEntry( + "http://m1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NEW, "meta", "data", function (entry) { + // Check the default + equal(entry.isForcedValid, false); + + // Forced valid and confirm + entry.forceValidFor(2); + do_timeout(1000, function () { + equal(entry.isForcedValid, true); + mc.fired(); + }); + + // Confirm the timeout occurs + do_timeout(3000, function () { + equal(entry.isForcedValid, false); + mc.fired(); + }); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-28-last-access-attrs.js b/netwerk/test/unit/test_cache2-28-last-access-attrs.js new file mode 100644 index 0000000000..2cae818736 --- /dev/null +++ b/netwerk/test/unit/test_cache2-28-last-access-attrs.js @@ -0,0 +1,46 @@ +"use strict"; + +function run_test() { + do_get_profile(); + function NowSeconds() { + return parseInt(new Date().getTime() / 1000); + } + function do_check_time(t, min, max) { + Assert.ok(t >= min); + Assert.ok(t <= max); + } + + var timeStart = NowSeconds(); + + asyncOpenCacheEntry( + "http://t/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "m", "d", function (entry) { + var firstOpen = NowSeconds(); + Assert.equal(entry.fetchCount, 1); + do_check_time(entry.lastFetched, timeStart, firstOpen); + do_check_time(entry.lastModified, timeStart, firstOpen); + + do_timeout(2000, () => { + asyncOpenCacheEntry( + "http://t/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "m", "d", function (entry) { + var secondOpen = NowSeconds(); + Assert.equal(entry.fetchCount, 2); + do_check_time(entry.lastFetched, firstOpen, secondOpen); + do_check_time(entry.lastModified, timeStart, firstOpen); + + finish_cache2_test(); + }) + ); + }); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js b/netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js new file mode 100644 index 0000000000..f25477f714 --- /dev/null +++ b/netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js @@ -0,0 +1,42 @@ +"use strict"; + +function run_test() { + do_get_profile(); + function NowSeconds() { + return parseInt(new Date().getTime() / 1000); + } + function do_check_time(a, b) { + Assert.ok(Math.abs(a - b) < 0.5); + } + + asyncOpenCacheEntry( + "http://t/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "m", "d", function (entry) { + var now1 = NowSeconds(); + Assert.equal(entry.fetchCount, 1); + do_check_time(entry.lastFetched, now1); + do_check_time(entry.lastModified, now1); + + do_timeout(2000, () => { + asyncOpenCacheEntry( + "http://t/", + "disk", + Ci.nsICacheStorage.OPEN_SECRETLY, + null, + new OpenCallback(NORMAL, "m", "d", function (entry) { + Assert.equal(entry.fetchCount, 1); + do_check_time(entry.lastFetched, now1); + do_check_time(entry.lastModified, now1); + + finish_cache2_test(); + }) + ); + }); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js b/netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js new file mode 100644 index 0000000000..014060ca83 --- /dev/null +++ b/netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js @@ -0,0 +1,74 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits +This test is using a resumable response. +- with a profile, set max-entry-size to 0 +- first channel makes a request for a resumable response +- second channel makes a request for the same resource, concurrent read happens +- first channel sets predicted data size on the entry, it's doomed +- second channel now must engage interrupted concurrent write algorithm and read the content again from the network +- both channels must deliver full content w/o errors + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "" + responseBody.length); + if (metadata.hasHeader("If-Range")) { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader("Content-Range", "0-12/13"); + } + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 0); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function () { + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen(new ChannelListener(firstTimeThrough, null)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen(new ChannelListener(secondTimeThrough, null)); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); +} + +function secondTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js b/netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js new file mode 100644 index 0000000000..0bb65ea534 --- /dev/null +++ b/netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js @@ -0,0 +1,78 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits. +This test is using a non-resumable response. +- with a profile, set max-entry-size to 0 +- first channel makes a request for a non-resumable (chunked) response +- second channel makes a request for the same resource, concurrent read is bypassed (non-resumable response) +- first channel writes first bytes to the cache output stream, but that fails because of the max-entry-size limit and entry is doomed +- cache entry output stream is closed +- second channel gets the entry, opening the input stream must fail +- second channel must read the content again from the network +- both channels must deliver full content w/o errors + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "c\r\ndata reached\r\n3\r\nhej\r\n0\r\n\r\n"; +const responseBodyDecoded = "data reachedhej"; + +function contentHandler(metadata, response) { + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(responseBody); + response.finish(); +} + +function run_test() { + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 0); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function () { + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen( + new ChannelListener(firstTimeThrough, null, CL_ALLOW_UNKNOWN_CL) + ); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen( + new ChannelListener(secondTimeThrough, null, CL_ALLOW_UNKNOWN_CL) + ); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBodyDecoded); +} + +function secondTimeThrough(request, buffer) { + Assert.equal(buffer, responseBodyDecoded); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js b/netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js new file mode 100644 index 0000000000..2c67127fef --- /dev/null +++ b/netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js @@ -0,0 +1,93 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits. +This is enhancement of 29a test, this test checks that cocurrency is resumed when the first channel is interrupted +in the middle of reading and the second channel already consumed some content from the cache entry. +This test is using a resumable response. +- with a profile, set max-entry-size to 1 (=1024 bytes) +- first channel makes a request for a resumable response +- second channel makes a request for the same resource, concurrent read happens +- first channel sets predicted data size on the entry with every chunk, it's doomed on 1024 +- second channel now must engage interrupted concurrent write algorithm and read the rest of the content from the network +- both channels must deliver full content w/o errors + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +// need something bigger than 1024 bytes +const responseBody = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "" + responseBody.length); + if (metadata.hasHeader("If-Range")) { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + + let len = responseBody.length; + response.setHeader("Content-Range", "0-" + (len - 1) + "/" + len); + } + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + // Static check + Assert.ok(responseBody.length > 1024); + + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function () { + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen(new ChannelListener(firstTimeThrough, null)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen(new ChannelListener(secondTimeThrough, null)); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); +} + +function secondTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js b/netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js new file mode 100644 index 0000000000..e7f96a4279 --- /dev/null +++ b/netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js @@ -0,0 +1,93 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits. +This is enhancement of 29c test, this test checks that a corrupted 206 response is correctly handled (no crashes or asserion failures) +This test is using a resumable response. +- with a profile, set max-entry-size to 1 (=1024 bytes) +- first channel makes a request for a resumable response +- second channel makes a request for the same resource, concurrent read happens +- first channel sets predicted data size on the entry with every chunk, it's doomed on 1024 +- second channel now must engage interrupted concurrent write algorithm and read the rest of the content from the network +- the response to the range request is broken (bad Content-Range header) +- the first must deliver full content w/o errors +- the second channel must correctly fail + +*/ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +// need something bigger than 1024 bytes +const responseBody = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "" + responseBody.length); + if (metadata.hasHeader("If-Range")) { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + // Deliberately broken response header to trigger corrupted content error on the second channel + response.setHeader("Content-Range", "0-1/2"); + } + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + // Static check + Assert.ok(responseBody.length > 1024); + + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function () { + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen(new ChannelListener(firstTimeThrough, null)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen( + new ChannelListener(secondTimeThrough, null, CL_EXPECT_FAILURE) + ); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); +} + +function secondTimeThrough(request, buffer) { + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js b/netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js new file mode 100644 index 0000000000..f960236504 --- /dev/null +++ b/netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js @@ -0,0 +1,88 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits. +This is enhancement of 29c test, this test checks that a corrupted 206 response is correctly handled (no crashes or asserion failures) +This test is using a resumable response. +- with a profile, set max-entry-size to 1 (=1024 bytes) +- first channel makes a request for a resumable response +- second channel makes a request for the same resource, concurrent read happens +- first channel sets predicted data size on the entry with every chunk, it's doomed on 1024 +- second channel now must engage interrupted concurrent write algorithm and read the rest of the content from the network +- the response to the range request is plain 200 +- the first must deliver full content w/o errors +- the second channel must correctly fail + +*/ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +// need something bigger than 1024 bytes +const responseBody = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "" + responseBody.length); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + // Static check + Assert.ok(responseBody.length > 1024); + + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function () { + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen(new ChannelListener(firstTimeThrough, null)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen( + new ChannelListener(secondTimeThrough, null, CL_EXPECT_FAILURE) + ); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); +} + +function secondTimeThrough(request, buffer) { + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_cache2-30a-entry-pinning.js b/netwerk/test/unit/test_cache2-30a-entry-pinning.js new file mode 100644 index 0000000000..a63503c266 --- /dev/null +++ b/netwerk/test/unit/test_cache2-30a-entry-pinning.js @@ -0,0 +1,39 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://a/", + "pin", + Ci.nsICacheStorage.OPEN_TRUNCATE, + Services.loadContextInfo.default, + new OpenCallback(NEW | WAITFORWRITE, "a1m", "a1d", function (entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + // Now clear the whole cache + Services.cache2.clear(); + + // The pinned entry should be intact + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-30b-pinning-storage-clear.js b/netwerk/test/unit/test_cache2-30b-pinning-storage-clear.js new file mode 100644 index 0000000000..86708da828 --- /dev/null +++ b/netwerk/test/unit/test_cache2-30b-pinning-storage-clear.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var lci = Services.loadContextInfo.default; + + // Open a pinned entry for write, write + asyncOpenCacheEntry( + "http://a/", + "pin", + Ci.nsICacheStorage.OPEN_TRUNCATE, + lci, + new OpenCallback(NEW | WAITFORWRITE, "a1m", "a1d", function (entry) { + // Now clear the disk storage, that should leave the pinned entry in the cache + var diskStorage = getCacheStorage("disk", lci); + diskStorage.asyncEvictStorage(null); + + // Open for read and check, it should still be there + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + // Now clear the pinning storage, entry should be gone + var pinningStorage = getCacheStorage("pin", lci); + pinningStorage.asyncEvictStorage(null); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NEW, "", "", function (entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js b/netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js new file mode 100644 index 0000000000..73dfee0f6b --- /dev/null +++ b/netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js @@ -0,0 +1,185 @@ +/* + +This is a complex test checking the internal "deferred doom" functionality in both CacheEntry and CacheFileHandle. + +- We create a batch of 10 non-pinned and 10 pinned entries, write something to them. +- Then we purge them from memory, so they have to reload from disk. +- After that the IO thread is suspended not to process events on the READ (3) level. This forces opening operation and eviction + sync operations happen before we know actual pinning status of already cached entries. +- We async-open the same batch of the 10+10 entries again, all should open as existing with the expected, previously stored + content +- After all these entries are made to open, we clear the cache. This does some synchronous operations on the entries + being open and also on the handles being in an already open state (but before the entry metadata has started to be read.) + Expected is to leave the pinned entries only. +- Now, we resume the IO thread, so it start reading. One could say this is a hack, but this can very well happen in reality + on slow disk or when a large number of entries is about to be open at once. Suspending the IO thread is just doing this + simulation is a fully deterministic way and actually very easily and elegantly. +- After the resume we want to open all those 10+10 entries once again (no purgin involved this time.). It is expected + to open all the pinning entries intact and loose all the non-pinned entries (get them as new and empty again.) + +*/ + +"use strict"; + +const kENTRYCOUNT = 10; + +function log_(msg) { + if (true) { + dump(">>>>>>>>>>>>> " + msg + "\n"); + } +} + +function run_test() { + do_get_profile(); + + var lci = Services.loadContextInfo.default; + var testingInterface = Services.cache2.QueryInterface(Ci.nsICacheTesting); + Assert.ok(testingInterface); + + var mc = new MultipleCallbacks( + 1, + function () { + // (2) + + mc = new MultipleCallbacks(1, finish_cache2_test); + // Release all references to cache entries so that they can be purged + // Calling gc() four times is needed to force it to actually release + // entries that are obviously unreferenced. Yeah, I know, this is wacky... + gc(); + gc(); + executeSoon(() => { + gc(); + gc(); + log_("purging"); + + // Invokes cacheservice:purge-memory-pools when done. + Services.cache2.purgeFromMemory( + Ci.nsICacheStorageService.PURGE_EVERYTHING + ); // goes to (3) + }); + }, + true + ); + + // (1), here we start + + var i; + for (i = 0; i < kENTRYCOUNT; ++i) { + log_("first set of opens"); + + // Callbacks 1-20 + mc.add(); + asyncOpenCacheEntry( + "http://pinned" + i + "/", + "pin", + Ci.nsICacheStorage.OPEN_TRUNCATE, + lci, + new OpenCallback(NEW | WAITFORWRITE, "m" + i, "p" + i, function (entry) { + mc.fired(); + }) + ); + + mc.add(); + asyncOpenCacheEntry( + "http://common" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + lci, + new OpenCallback(NEW | WAITFORWRITE, "m" + i, "d" + i, function (entry) { + mc.fired(); + }) + ); + } + + mc.fired(); // Goes to (2) + + Services.obs.addObserver( + { + observe(subject, topic, data) { + // (3) + + log_("after purge, second set of opens"); + // Prevent the I/O thread from reading the data. We first want to schedule clear of the cache. + // This deterministically emulates a slow hard drive. + testingInterface.suspendCacheIOThread(3); + + // All entries should load + // Callbacks 21-40 + for (i = 0; i < kENTRYCOUNT; ++i) { + mc.add(); + asyncOpenCacheEntry( + "http://pinned" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NORMAL, "m" + i, "p" + i, function (entry) { + mc.fired(); + }) + ); + + // Unfortunately we cannot ensure that entries existing in the cache will be delivered to the consumer + // when soon after are evicted by some cache API call. It's better to not ensure getting an entry + // than allowing to get an entry that was just evicted from the cache. Entries may be delievered + // as new, but are already doomed. Output stream cannot be openned, or the file handle is already + // writing to a doomed file. + // + // The API now just ensures that entries removed by any of the cache eviction APIs are never more + // available to consumers. + mc.add(); + asyncOpenCacheEntry( + "http://common" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(MAYBE_NEW | DOOMED, "m" + i, "d" + i, function ( + entry + ) { + mc.fired(); + }) + ); + } + + log_("clearing"); + // Now clear everything except pinned, all entries are in state of reading + Services.cache2.clear(); + log_("cleared"); + + // Resume reading the cache data, only now the pinning status on entries will be discovered, + // the deferred dooming code will trigger. + testingInterface.resumeCacheIOThread(); + + log_("third set of opens"); + // Now open again. Pinned entries should be there, disk entries should be the renewed entries. + // Callbacks 41-60 + for (i = 0; i < kENTRYCOUNT; ++i) { + mc.add(); + asyncOpenCacheEntry( + "http://pinned" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NORMAL, "m" + i, "p" + i, function (entry) { + mc.fired(); + }) + ); + + mc.add(); + asyncOpenCacheEntry( + "http://common" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NEW, "m2" + i, "d2" + i, function (entry) { + mc.fired(); + }) + ); + } + + mc.fired(); // Finishes this test + }, + }, + "cacheservice:purge-memory-pools" + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js b/netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js new file mode 100644 index 0000000000..fd4622f3f4 --- /dev/null +++ b/netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js @@ -0,0 +1,148 @@ +/* + +This test exercises the CacheFileContextEvictor::WasEvicted API and code using it. + +- We store 10+10 (pinned and non-pinned) entries to the cache, wait for them being written. +- Then we purge the memory pools. +- Now the IO thread is suspended on the EVICT (7) level to prevent actual deletion of the files. +- Index is disabled. +- We do clear() of the cache, this creates the "ce_*" file and posts to the EVICT level + the eviction loop mechanics. +- We open again those 10+10 entries previously stored. +- IO is resumed +- We expect to get all the pinned and + loose all the non-pinned (common) entries. + +*/ + +"use strict"; + +const kENTRYCOUNT = 10; + +function log_(msg) { + if (true) { + dump(">>>>>>>>>>>>> " + msg + "\n"); + } +} + +function run_test() { + do_get_profile(); + + var lci = Services.loadContextInfo.default; + var testingInterface = Services.cache2.QueryInterface(Ci.nsICacheTesting); + Assert.ok(testingInterface); + + var mc = new MultipleCallbacks( + 1, + function () { + // (2) + + mc = new MultipleCallbacks(1, finish_cache2_test); + // Release all references to cache entries so that they can be purged + // Calling gc() four times is needed to force it to actually release + // entries that are obviously unreferenced. Yeah, I know, this is wacky... + gc(); + gc(); + executeSoon(() => { + gc(); + gc(); + log_("purging"); + + // Invokes cacheservice:purge-memory-pools when done. + Services.cache2.purgeFromMemory( + Ci.nsICacheStorageService.PURGE_EVERYTHING + ); // goes to (3) + }); + }, + true + ); + + // (1), here we start + + log_("first set of opens"); + var i; + for (i = 0; i < kENTRYCOUNT; ++i) { + // Callbacks 1-20 + mc.add(); + asyncOpenCacheEntry( + "http://pinned" + i + "/", + "pin", + Ci.nsICacheStorage.OPEN_TRUNCATE, + lci, + new OpenCallback(NEW | WAITFORWRITE, "m" + i, "p" + i, function (entry) { + mc.fired(); + }) + ); + + mc.add(); + asyncOpenCacheEntry( + "http://common" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + lci, + new OpenCallback(NEW | WAITFORWRITE, "m" + i, "d" + i, function (entry) { + mc.fired(); + }) + ); + } + + mc.fired(); // Goes to (2) + + Services.obs.addObserver( + { + observe(subject, topic, data) { + // (3) + + log_("after purge"); + // Prevent the I/O thread from evicting physically the data. We first want to re-open the entries. + // This deterministically emulates a slow hard drive. + testingInterface.suspendCacheIOThread(7); + + log_("clearing"); + // Now clear everything except pinned. Stores the "ce_*" file and schedules background eviction. + Services.cache2.clear(); + log_("cleared"); + + log_("second set of opens"); + // Now open again. Pinned entries should be there, disk entries should be the renewed entries. + // Callbacks 21-40 + for (i = 0; i < kENTRYCOUNT; ++i) { + mc.add(); + asyncOpenCacheEntry( + "http://pinned" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NORMAL, "m" + i, "p" + i, function (entry) { + mc.fired(); + }) + ); + + mc.add(); + asyncOpenCacheEntry( + "http://common" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NEW, "m2" + i, "d2" + i, function (entry) { + mc.fired(); + }) + ); + } + + // Resume IO, this will just pop-off the CacheFileContextEvictor::EvictEntries() because of + // an early check on CacheIOThread::YieldAndRerun() in that method. + // CacheFileIOManager::OpenFileInternal should now run and CacheFileContextEvictor::WasEvicted + // should be checked on. + log_("resuming"); + testingInterface.resumeCacheIOThread(); + log_("resumed"); + + mc.fired(); // Finishes this test + }, + }, + "cacheservice:purge-memory-pools" + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-31-visit-all.js b/netwerk/test/unit/test_cache2-31-visit-all.js new file mode 100644 index 0000000000..481916afcc --- /dev/null +++ b/netwerk/test/unit/test_cache2-31-visit-all.js @@ -0,0 +1,88 @@ +"use strict"; + +function run_test() { + getCacheStorage("disk"); + var lcis = [ + Services.loadContextInfo.default, + Services.loadContextInfo.custom(false, { userContextId: 1 }), + Services.loadContextInfo.custom(false, { userContextId: 2 }), + Services.loadContextInfo.custom(false, { userContextId: 3 }), + ]; + + do_get_profile(); + + var mc = new MultipleCallbacks( + 8, + function () { + executeSoon(function () { + var expectedConsumption = 8192; + var entries = [ + { uri: "http://a/", lci: lcis[0] }, // default + { uri: "http://b/", lci: lcis[0] }, // default + { uri: "http://a/", lci: lcis[1] }, // user Context 1 + { uri: "http://b/", lci: lcis[1] }, // user Context 1 + { uri: "http://a/", lci: lcis[2] }, // user Context 2 + { uri: "http://b/", lci: lcis[2] }, // user Context 2 + { uri: "http://a/", lci: lcis[3] }, // user Context 3 + { uri: "http://b/", lci: lcis[3] }, + ]; // user Context 3 + + Services.cache2.asyncVisitAllStorages( + // Test should store 8 entries across 4 originAttributes + new VisitCallback(8, expectedConsumption, entries, function () { + Services.cache2.asyncVisitAllStorages( + // Still 8 entries expected, now don't walk them + new VisitCallback(8, expectedConsumption, null, function () { + finish_cache2_test(); + }), + false + ); + }), + true + ); + }); + }, + true + ); + + // Add two cache entries for each originAttributes. + for (var i = 0; i < lcis.length; i++) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lcis[i], + new OpenCallback(NEW, "a1m", "a1d", function (entry) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lcis[i], + new OpenCallback(NORMAL, "a1m", "a1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lcis[i], + new OpenCallback(NEW, "b1m", "b1d", function (entry) { + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lcis[i], + new OpenCallback(NORMAL, "b1m", "b1d", function (entry) { + mc.fired(); + }) + ); + }) + ); + } + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-32-clear-origin.js b/netwerk/test/unit/test_cache2-32-clear-origin.js new file mode 100644 index 0000000000..8358f2f045 --- /dev/null +++ b/netwerk/test/unit/test_cache2-32-clear-origin.js @@ -0,0 +1,71 @@ +"use strict"; + +const URL = "http://example.net"; +const URL2 = "http://foo.bar"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + URL + "/a", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "e1m", "e1d", function (entry) { + asyncOpenCacheEntry( + URL + "/a", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "e1m", "e1d", function (entry) { + asyncOpenCacheEntry( + URL2 + "/a", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "f1m", "f1d", function (entry) { + asyncOpenCacheEntry( + URL2 + "/a", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "f1m", "f1d", function (entry) { + var url = Services.io.newURI(URL); + var principal = + Services.scriptSecurityManager.createContentPrincipal( + url, + {} + ); + + Services.cache2.clearOrigin(principal); + + asyncOpenCacheEntry( + URL + "/a", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "e1m", "e1d", function (entry) { + asyncOpenCacheEntry( + URL2 + "/a", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "f1m", "f1d", function ( + entry + ) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache_204_response.js b/netwerk/test/unit/test_cache_204_response.js new file mode 100644 index 0000000000..74181f6ed1 --- /dev/null +++ b/netwerk/test/unit/test_cache_204_response.js @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* +Test if 204 response is cached. +1. Make first http request and return a 204 response. +2. Check if the first response is not cached. +3. Make second http request and check if the response is cached. + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +function test_handler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-control", "max-age=9999", false); + response.setStatusLine(metadata.httpVersion, 204, "No Content"); +} + +function make_channel(url) { + let channel = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return channel; +} + +async function get_response(channel, fromCache) { + return new Promise(resolve => { + channel.asyncOpen( + new ChannelListener((request, buffer, ctx, isFromCache) => { + ok(fromCache == isFromCache, `got response from cache = ${fromCache}`); + resolve(); + }) + ); + }); +} + +async function stop_server(httpserver) { + return new Promise(resolve => { + httpserver.stop(resolve); + }); +} + +add_task(async function () { + let httpserver = new HttpServer(); + httpserver.registerPathHandler("/testdir", test_handler); + httpserver.start(-1); + const PORT = httpserver.identity.primaryPort; + const URI = `http://localhost:${PORT}/testdir`; + + await get_response(make_channel(URI, "GET"), false); + await get_response(make_channel(URI, "GET"), true); + + await stop_server(httpserver); +}); diff --git a/netwerk/test/unit/test_cache_jar.js b/netwerk/test/unit/test_cache_jar.js new file mode 100644 index 0000000000..6e52203f2a --- /dev/null +++ b/netwerk/test/unit/test_cache_jar.js @@ -0,0 +1,103 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserv.identity.primaryPort + "/cached"; +}); + +var httpserv = null; +var handlers_called = 0; + +function cached_handler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + var body = "0123456789"; + response.bodyOutputStream.write(body, body.length); + handlers_called++; +} + +function makeChan(url, inIsolatedMozBrowser, userContextId) { + var chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadInfo.originAttributes = { inIsolatedMozBrowser, userContextId }; + return chan; +} + +// [inIsolatedMozBrowser, userContextId, expected_handlers_called] +var firstTests = [ + [false, 0, 1], + [true, 0, 1], + [false, 1, 1], + [true, 1, 1], +]; +var secondTests = [ + [false, 0, 0], + [true, 0, 0], + [false, 1, 1], + [true, 1, 0], +]; + +async function run_all_tests() { + for (let test of firstTests) { + handlers_called = 0; + await test_channel(...test); + } + + // We can't easily cause webapp data to be cleared from the child process, so skip + // the rest of these tests. + let procType = Services.appinfo.processType; + if (procType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { + return; + } + + Services.clearData.deleteDataFromOriginAttributesPattern({ + userContextId: 1, + }); + + for (let test of secondTests) { + handlers_called = 0; + await test_channel(...test); + } +} + +function run_test() { + do_get_profile(); + + do_test_pending(); + + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpserv = new HttpServer(); + httpserv.registerPathHandler("/cached", cached_handler); + httpserv.start(-1); + run_all_tests().then(() => { + do_test_finished(); + }); +} + +function test_channel(inIsolatedMozBrowser, userContextId, expected) { + return new Promise(resolve => { + var chan = makeChan(URL, inIsolatedMozBrowser, userContextId); + chan.asyncOpen( + new ChannelListener(doneFirstLoad.bind(null, resolve), expected) + ); + }); +} + +function doneFirstLoad(resolve, req, buffer, expected) { + // Load it again, make sure it hits the cache + var oa = req.loadInfo.originAttributes; + var chan = makeChan(URL, oa.isInIsolatedMozBrowserElement, oa.userContextId); + chan.asyncOpen( + new ChannelListener(doneSecondLoad.bind(null, resolve), expected) + ); +} + +function doneSecondLoad(resolve, req, buffer, expected) { + Assert.equal(handlers_called, expected); + resolve(); +} diff --git a/netwerk/test/unit/test_cacheflags.js b/netwerk/test/unit/test_cacheflags.js new file mode 100644 index 0000000000..b0350bbdca --- /dev/null +++ b/netwerk/test/unit/test_cacheflags.js @@ -0,0 +1,435 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +httpserver.start(-1); + +// Need to randomize, because apparently no one clears our cache +var suffix = Math.random(); +var httpBase = "http://localhost:" + httpserver.identity.primaryPort; +var shortexpPath = "/shortexp" + suffix; +var longexpPath = "/longexp/" + suffix; +var longexp2Path = "/longexp/2/" + suffix; +var nocachePath = "/nocache" + suffix; +var nostorePath = "/nostore" + suffix; +var test410Path = "/test410" + suffix; +var test404Path = "/test404" + suffix; + +var PrivateBrowsingLoadContext = Cu.createPrivateLoadContext(); + +function make_channel(url, flags, usePrivateBrowsing) { + var securityFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; + + var uri = Services.io.newURI(url); + var principal = Services.scriptSecurityManager.createContentPrincipal(uri, { + privateBrowsingId: usePrivateBrowsing ? 1 : 0, + }); + + var req = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + req.loadFlags = flags; + if (usePrivateBrowsing) { + req.notificationCallbacks = PrivateBrowsingLoadContext; + } + return req; +} + +function Test( + path, + flags, + expectSuccess, + readFromCache, + hitServer, + usePrivateBrowsing /* defaults to false */ +) { + this.path = path; + this.flags = flags; + this.expectSuccess = expectSuccess; + this.readFromCache = readFromCache; + this.hitServer = hitServer; + this.usePrivateBrowsing = usePrivateBrowsing; +} + +Test.prototype = { + flags: 0, + expectSuccess: true, + readFromCache: false, + hitServer: true, + usePrivateBrowsing: false, + _buffer: "", + _isFromCache: false, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + var cachingChannel = request.QueryInterface(Ci.nsICacheInfoChannel); + this._isFromCache = request.isPending() && cachingChannel.isFromCache(); + }, + + onDataAvailable(request, stream, offset, count) { + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + + onStopRequest(request, status) { + Assert.equal(Components.isSuccessCode(status), this.expectSuccess); + Assert.equal(this._isFromCache, this.readFromCache); + Assert.equal(gHitServer, this.hitServer); + + do_timeout(0, run_next_test); + }, + + run() { + dump( + "Running:" + + "\n " + + this.path + + "\n " + + this.flags + + "\n " + + this.expectSuccess + + "\n " + + this.readFromCache + + "\n " + + this.hitServer + + "\n" + ); + gHitServer = false; + var channel = make_channel(this.path, this.flags, this.usePrivateBrowsing); + channel.asyncOpen(this); + }, +}; + +var gHitServer = false; + +var gTests = [ + new Test( + httpBase + shortexpPath, + 0, + true, // expect success + false, // read from cache + true, // hit server + true + ), // USE PRIVATE BROWSING, so not cached for later requests + new Test( + httpBase + shortexpPath, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + shortexpPath, + 0, + true, // expect success + true, // read from cache + true + ), // hit server + new Test( + httpBase + shortexpPath, + Ci.nsIRequest.LOAD_BYPASS_CACHE, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + shortexpPath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE, + false, // expect success + false, // read from cache + false + ), // hit server + new Test( + httpBase + shortexpPath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_NEVER, + true, // expect success + true, // read from cache + false + ), // hit server + new Test( + httpBase + shortexpPath, + Ci.nsIRequest.LOAD_FROM_CACHE, + true, // expect success + true, // read from cache + false + ), // hit server + + new Test( + httpBase + longexpPath, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + longexpPath, + 0, + true, // expect success + true, // read from cache + false + ), // hit server + new Test( + httpBase + longexpPath, + Ci.nsIRequest.LOAD_BYPASS_CACHE, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + longexpPath, + Ci.nsIRequest.VALIDATE_ALWAYS, + true, // expect success + true, // read from cache + true + ), // hit server + new Test( + httpBase + longexpPath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE, + true, // expect success + true, // read from cache + false + ), // hit server + new Test( + httpBase + longexpPath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_NEVER, + true, // expect success + true, // read from cache + false + ), // hit server + new Test( + httpBase + longexpPath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_ALWAYS, + false, // expect success + false, // read from cache + false + ), // hit server + new Test( + httpBase + longexpPath, + Ci.nsIRequest.LOAD_FROM_CACHE, + true, // expect success + true, // read from cache + false + ), // hit server + + new Test( + httpBase + longexp2Path, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + longexp2Path, + 0, + true, // expect success + true, // read from cache + false + ), // hit server + + new Test( + httpBase + nocachePath, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + nocachePath, + 0, + true, // expect success + true, // read from cache + true + ), // hit server + new Test( + httpBase + nocachePath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE, + false, // expect success + false, // read from cache + false + ), // hit server + + // CACHE2: mayhemer - entry is doomed... I think the logic is wrong, we should not doom them + // as they are not valid, but take them as they need to reval + /* + new Test(httpBase + nocachePath, Ci.nsIRequest.LOAD_FROM_CACHE, + true, // expect success + true, // read from cache + false), // hit server + */ + + // LOAD_ONLY_FROM_CACHE would normally fail (because no-cache forces + // a validation), but VALIDATE_NEVER should override that. + new Test( + httpBase + nocachePath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_NEVER, + true, // expect success + true, // read from cache + false + ), // hit server + + // ... however, no-cache over ssl should act like no-store and force + // a validation (and therefore failure) even if VALIDATE_NEVER is + // set. + /* XXX bug 466524: We can't currently start an ssl server in xpcshell tests, + so this test is currently disabled. + new Test(httpsBase + nocachePath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | + Ci.nsIRequest.VALIDATE_NEVER, + false, // expect success + false, // read from cache + false) // hit server + */ + + new Test( + httpBase + nostorePath, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + nostorePath, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + nostorePath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE, + false, // expect success + false, // read from cache + false + ), // hit server + new Test( + httpBase + nostorePath, + Ci.nsIRequest.LOAD_FROM_CACHE, + true, // expect success + true, // read from cache + false + ), // hit server + // no-store should force the validation (and therefore failure, with + // LOAD_ONLY_FROM_CACHE) even if VALIDATE_NEVER is set. + new Test( + httpBase + nostorePath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_NEVER, + false, // expect success + false, // read from cache + false + ), // hit server + + new Test( + httpBase + test410Path, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + test410Path, + 0, + true, // expect success + true, // read from cache + false + ), // hit server + + new Test( + httpBase + test404Path, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + test404Path, + 0, + true, // expect success + false, // read from cache + true + ), // hit server +]; + +function run_next_test() { + if (!gTests.length) { + httpserver.stop(do_test_finished); + return; + } + + var test = gTests.shift(); + test.run(); +} + +function handler(httpStatus, metadata, response) { + gHitServer = true; + let etag; + try { + etag = metadata.getHeader("If-None-Match"); + } catch (ex) { + etag = ""; + } + if (etag == "testtag") { + // Allow using the cached data + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + } else { + response.setStatusLine(metadata.httpVersion, httpStatus, "Useless Phrase"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "testtag", false); + const body = "data"; + response.bodyOutputStream.write(body, body.length); + } +} + +function nocache_handler(metadata, response) { + response.setHeader("Cache-Control", "no-cache", false); + handler(200, metadata, response); +} + +function nostore_handler(metadata, response) { + response.setHeader("Cache-Control", "no-store", false); + handler(200, metadata, response); +} + +function test410_handler(metadata, response) { + handler(410, metadata, response); +} + +function test404_handler(metadata, response) { + handler(404, metadata, response); +} + +function shortexp_handler(metadata, response) { + response.setHeader("Cache-Control", "max-age=0", false); + handler(200, metadata, response); +} + +function longexp_handler(metadata, response) { + response.setHeader("Cache-Control", "max-age=10000", false); + handler(200, metadata, response); +} + +// test spaces around max-age value token +function longexp2_handler(metadata, response) { + response.setHeader("Cache-Control", "max-age = 10000", false); + handler(200, metadata, response); +} + +function run_test() { + httpserver.registerPathHandler(shortexpPath, shortexp_handler); + httpserver.registerPathHandler(longexpPath, longexp_handler); + httpserver.registerPathHandler(longexp2Path, longexp2_handler); + httpserver.registerPathHandler(nocachePath, nocache_handler); + httpserver.registerPathHandler(nostorePath, nostore_handler); + httpserver.registerPathHandler(test410Path, test410_handler); + httpserver.registerPathHandler(test404Path, test404_handler); + + run_next_test(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_captive_portal_service.js b/netwerk/test/unit/test_captive_portal_service.js new file mode 100644 index 0000000000..1488e9d41f --- /dev/null +++ b/netwerk/test/unit/test_captive_portal_service.js @@ -0,0 +1,323 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +let httpserver = null; +XPCOMUtils.defineLazyGetter(this, "cpURI", function () { + return ( + "http://localhost:" + httpserver.identity.primaryPort + "/captive.html" + ); +}); + +const SUCCESS_STRING = + '<meta http-equiv="refresh" content="0;url=https://support.mozilla.org/kb/captive-portal"/>'; +let cpResponse = SUCCESS_STRING; +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write(cpResponse, cpResponse.length); +} + +const PREF_CAPTIVE_ENABLED = "network.captive-portal-service.enabled"; +const PREF_CAPTIVE_TESTMODE = "network.captive-portal-service.testMode"; +const PREF_CAPTIVE_ENDPOINT = "captivedetect.canonicalURL"; +const PREF_CAPTIVE_MINTIME = "network.captive-portal-service.minInterval"; +const PREF_CAPTIVE_MAXTIME = "network.captive-portal-service.maxInterval"; +const PREF_DNS_NATIVE_IS_LOCALHOST = "network.dns.native-is-localhost"; + +const cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService( + Ci.nsICaptivePortalService +); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref(PREF_CAPTIVE_ENABLED); + Services.prefs.clearUserPref(PREF_CAPTIVE_TESTMODE); + Services.prefs.clearUserPref(PREF_CAPTIVE_ENDPOINT); + Services.prefs.clearUserPref(PREF_CAPTIVE_MINTIME); + Services.prefs.clearUserPref(PREF_CAPTIVE_MAXTIME); + Services.prefs.clearUserPref(PREF_DNS_NATIVE_IS_LOCALHOST); + + await new Promise(resolve => { + httpserver.stop(resolve); + }); +}); + +function observerPromise(topic) { + return new Promise(resolve => { + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == topic) { + Services.obs.removeObserver(observer, topic); + resolve(aData); + } + }, + }; + Services.obs.addObserver(observer, topic); + }); +} + +add_task(function setup() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/captive.html", contentHandler); + httpserver.start(-1); + + Services.prefs.setCharPref(PREF_CAPTIVE_ENDPOINT, cpURI); + Services.prefs.setIntPref(PREF_CAPTIVE_MINTIME, 50); + Services.prefs.setIntPref(PREF_CAPTIVE_MAXTIME, 100); + Services.prefs.setBoolPref(PREF_CAPTIVE_TESTMODE, true); + Services.prefs.setBoolPref(PREF_DNS_NATIVE_IS_LOCALHOST, true); +}); + +add_task(async function test_simple() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + let notification = observerPromise("network:captive-portal-connectivity"); + // The service is started by nsIOService when the pref becomes true. + // We might want to add a method to do this in the future. + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + let observerPayload = await notification; + equal(observerPayload, "clear"); + equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE); + + cpResponse = "other"; + notification = observerPromise("captive-portal-login"); + cps.recheckCaptivePortal(); + await notification; + equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL); + + cpResponse = SUCCESS_STRING; + notification = observerPromise("captive-portal-login-success"); + cps.recheckCaptivePortal(); + await notification; + equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL); +}); + +// This test redirects to another URL which returns the same content. +// It should still be interpreted as a captive portal. +add_task(async function test_redirect_success() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + httpserver.registerPathHandler("/succ.txt", (metadata, response) => { + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write(cpResponse, cpResponse.length); + }); + httpserver.registerPathHandler("/captive.html", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily"); + response.setHeader( + "Location", + `http://localhost:${httpserver.identity.primaryPort}/succ.txt` + ); + }); + + let notification = observerPromise("captive-portal-login").then( + () => "login" + ); + let succNotif = observerPromise("network:captive-portal-connectivity").then( + () => "connectivity" + ); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + let winner = await Promise.race([notification, succNotif]); + equal(winner, "login", "This should have been a login, not a success"); + equal( + cps.state, + Ci.nsICaptivePortalService.LOCKED_PORTAL, + "Should be locked after redirect to same text" + ); +}); + +// This redirects to another URI with a different content. +// We check that it triggers a captive portal login +add_task(async function test_redirect_bad() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + httpserver.registerPathHandler("/bad.txt", (metadata, response) => { + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write("bad", "bad".length); + }); + + httpserver.registerPathHandler("/captive.html", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily"); + response.setHeader( + "Location", + `http://localhost:${httpserver.identity.primaryPort}/bad.txt` + ); + }); + + let notification = observerPromise("captive-portal-login"); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + await notification; + equal( + cps.state, + Ci.nsICaptivePortalService.LOCKED_PORTAL, + "Should be locked after redirect to bad text" + ); +}); + +// This redirects to the same URI. +// We check that it triggers a captive portal login +add_task(async function test_redirect_loop() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + // This is actually a redirect loop + httpserver.registerPathHandler("/captive.html", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily"); + response.setHeader("Location", cpURI); + }); + + let notification = observerPromise("captive-portal-login"); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + await notification; + equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL); +}); + +// This redirects to a https URI. +// We check that it triggers a captive portal login +add_task(async function test_redirect_https() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Any kind of redirection should trigger the captive portal login. + httpserver.registerPathHandler("/captive.html", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily"); + response.setHeader("Location", `https://foo.example.com:${h2Port}/exit`); + }); + + let notification = observerPromise("captive-portal-login"); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + await notification; + equal( + cps.state, + Ci.nsICaptivePortalService.LOCKED_PORTAL, + "Should be locked after redirect to https" + ); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); +}); + +// This test uses a 511 status code to request a captive portal login +// We check that it triggers a captive portal login +// See RFC 6585 for details +add_task(async function test_511_error() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + httpserver.registerPathHandler("/captive.html", (metadata, response) => { + response.setStatusLine( + metadata.httpVersion, + 511, + "Network Authentication Required" + ); + cpResponse = '<meta http-equiv="refresh" content="0;url=/login">'; + contentHandler(metadata, response); + }); + + let notification = observerPromise("captive-portal-login"); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + await notification; + equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL); +}); + +// Any other 5xx HTTP error, is assumed to be an issue with the +// canonical web server, and should not trigger a captive portal login +add_task(async function test_generic_5xx_error() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + let requests = 0; + httpserver.registerPathHandler("/captive.html", (metadata, response) => { + if (requests++ === 0) { + // on first attempt, send 503 error + response.setStatusLine( + metadata.httpVersion, + 503, + "Internal Server Error" + ); + cpResponse = "<h1>Internal Server Error</h1>"; + } else { + // on retry, send canonical reply + cpResponse = SUCCESS_STRING; + } + contentHandler(metadata, response); + }); + + let notification = observerPromise("network:captive-portal-connectivity"); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + await notification; + equal(requests, 2); + equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE); +}); + +add_task(async function test_changed_notification() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + httpserver.registerPathHandler("/captive.html", contentHandler); + cpResponse = SUCCESS_STRING; + + let changedNotificationCount = 0; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + changedNotificationCount += 1; + }, + }; + Services.obs.addObserver( + observer, + "network:captive-portal-connectivity-changed" + ); + + let notification = observerPromise( + "network:captive-portal-connectivity-changed" + ); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + await notification; + equal(changedNotificationCount, 1); + equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE); + + notification = observerPromise("network:captive-portal-connectivity"); + cps.recheckCaptivePortal(); + await notification; + equal(changedNotificationCount, 1); + equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE); + + notification = observerPromise("captive-portal-login"); + cpResponse = "you are captive"; + cps.recheckCaptivePortal(); + await notification; + equal(changedNotificationCount, 1); + equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL); + + notification = observerPromise("captive-portal-login-success"); + cpResponse = SUCCESS_STRING; + cps.recheckCaptivePortal(); + await notification; + equal(changedNotificationCount, 2); + equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL); + + notification = observerPromise("captive-portal-login"); + cpResponse = "you are captive"; + cps.recheckCaptivePortal(); + await notification; + equal(changedNotificationCount, 2); + equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL); + + Services.obs.removeObserver( + observer, + "network:captive-portal-connectivity-changed" + ); +}); diff --git a/netwerk/test/unit/test_cert_info.js b/netwerk/test/unit/test_cert_info.js new file mode 100644 index 0000000000..abf5a1d634 --- /dev/null +++ b/netwerk/test/unit/test_cert_info.js @@ -0,0 +1,162 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +async function test_cert_failure(server_creator, https_proxy) { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + let server = new server_creator(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + + await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200); + resp.end("done"); + }); + let url; + if (server_creator == NodeHTTPServer) { + url = `http://localhost:${server.port()}/test`; + } else { + url = `https://localhost:${server.port()}/test`; + } + let chan = makeChan(url); + let req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + let secinfo = req.securityInfo; + if (server_creator == NodeHTTPServer) { + if (!https_proxy) { + Assert.equal(secinfo, null); + } else { + // In the case we are connecting to an insecure HTTP server + // through a secure proxy, nsHttpChannel will have the security + // info from the proxy. + // We will discuss this behavir in bug 1785777. + secinfo.QueryInterface(Ci.nsITransportSecurityInfo); + Assert.equal(secinfo.serverCert.commonName, " Proxy Test Cert"); + } + } else { + secinfo.QueryInterface(Ci.nsITransportSecurityInfo); + Assert.equal(secinfo.serverCert.commonName, " HTTP2 Test Cert"); + } +} + +add_task(async function test_http() { + await test_cert_failure(NodeHTTPServer, false); +}); + +add_task(async function test_https() { + await test_cert_failure(NodeHTTPSServer, false); +}); + +add_task(async function test_http2() { + await test_cert_failure(NodeHTTP2Server, false); +}); + +add_task(async function test_http_proxy_http_server() { + let proxy = new NodeHTTPProxyServer(); + await proxy.start(); + registerCleanupFunction(() => { + proxy.stop(); + }); + await test_cert_failure(NodeHTTPServer, false); +}); + +add_task(async function test_http_proxy_https_server() { + let proxy = new NodeHTTPProxyServer(); + await proxy.start(); + registerCleanupFunction(() => { + proxy.stop(); + }); + await test_cert_failure(NodeHTTPSServer, false); +}); + +add_task(async function test_http_proxy_http2_server() { + let proxy = new NodeHTTPProxyServer(); + await proxy.start(); + registerCleanupFunction(() => { + proxy.stop(); + }); + await test_cert_failure(NodeHTTP2Server, false); +}); + +add_task(async function test_https_proxy_http_server() { + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + registerCleanupFunction(() => { + proxy.stop(); + }); + await test_cert_failure(NodeHTTPServer, true); +}); + +add_task(async function test_https_proxy_https_server() { + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + registerCleanupFunction(() => { + proxy.stop(); + }); + await test_cert_failure(NodeHTTPSServer, true); +}); + +add_task(async function test_https_proxy_http2_server() { + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + registerCleanupFunction(() => { + proxy.stop(); + }); + await test_cert_failure(NodeHTTP2Server, true); +}); + +add_task(async function test_http2_proxy_http_server() { + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); + registerCleanupFunction(() => { + proxy.stop(); + }); + + await test_cert_failure(NodeHTTPServer, true); +}); + +add_task(async function test_http2_proxy_https_server() { + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); + registerCleanupFunction(() => { + proxy.stop(); + }); + + await test_cert_failure(NodeHTTPSServer, true); +}); + +add_task(async function test_http2_proxy_http2_server() { + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); + registerCleanupFunction(() => { + proxy.stop(); + }); + + await test_cert_failure(NodeHTTP2Server, true); +}); diff --git a/netwerk/test/unit/test_cert_verification_failure.js b/netwerk/test/unit/test_cert_verification_failure.js new file mode 100644 index 0000000000..11eb575dc1 --- /dev/null +++ b/netwerk/test/unit/test_cert_verification_failure.js @@ -0,0 +1,66 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +async function test_cert_failure(server_or_proxy, server_cert) { + let server = new server_or_proxy(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + let chan = makeChan(`https://localhost:${server.port()}/test`); + let req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)); + }); + equal(req.status, 0x805a1ff3); // SEC_ERROR_UNKNOWN_ISSUER + let secinfo = req.securityInfo; + secinfo.QueryInterface(Ci.nsITransportSecurityInfo); + if (server_cert) { + Assert.equal(secinfo.serverCert.commonName, " HTTP2 Test Cert"); + } else { + Assert.equal(secinfo.serverCert.commonName, " Proxy Test Cert"); + } +} + +add_task(async function test_https() { + await test_cert_failure(NodeHTTPSServer, true); +}); + +add_task(async function test_http2() { + await test_cert_failure(NodeHTTP2Server, true); +}); + +add_task(async function test_https_proxy() { + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + registerCleanupFunction(() => { + proxy.stop(); + }); + await test_cert_failure(NodeHTTPSServer, false); +}); + +add_task(async function test_http2_proxy() { + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); + registerCleanupFunction(() => { + proxy.stop(); + }); + + await test_cert_failure(NodeHTTPSServer, false); +}); diff --git a/netwerk/test/unit/test_channel_close.js b/netwerk/test/unit/test_channel_close.js new file mode 100644 index 0000000000..756675e101 --- /dev/null +++ b/netwerk/test/unit/test_channel_close.js @@ -0,0 +1,68 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var testpath = "/simple"; +var httpbody = "0123456789"; + +var live_channels = []; + +function run_test() { + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function () { + var local_channel; + + // Opened channel that has no remaining references on shutdown + local_channel = setupChannel(testpath); + local_channel.asyncOpen(new ChannelListener(checkRequest, local_channel)); + + // Opened channel that has no remaining references after being opened + setupChannel(testpath).asyncOpen(new ChannelListener(function () {}, null)); + + // Unopened channel that has remaining references on shutdown + live_channels.push(setupChannel(testpath)); + + // Opened channel that has remaining references on shutdown + live_channels.push(setupChannel(testpath)); + live_channels[1].asyncOpen( + new ChannelListener(checkRequestFinish, live_channels[1]) + ); + }); + + do_test_pending(); +} + +function setupChannel(path) { + var chan = NetUtil.newChannel({ + uri: URL + path, + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + return chan; +} + +function serverHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(httpbody, httpbody.length); +} + +function checkRequest(request, data, context) { + Assert.equal(data, httpbody); +} + +function checkRequestFinish(request, data, context) { + checkRequest(request, data, context); + httpserver.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_channel_long_domain.js b/netwerk/test/unit/test_channel_long_domain.js new file mode 100644 index 0000000000..1aa59412b3 --- /dev/null +++ b/netwerk/test/unit/test_channel_long_domain.js @@ -0,0 +1,17 @@ +// Tests that domains longer than 253 characters fail to load when pref is true + +add_task(async function test_long_domain_fails() { + Services.prefs.setBoolPref("network.dns.limit_253_chars", true); + let domain = "http://" + "a".repeat(254); + + let req = await new Promise(resolve => { + let chan = NetUtil.newChannel({ + uri: domain, + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)); + }); + Assert.equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST, "Request should fail"); + + Services.prefs.clearUserPref("network.dns.limit_253_chars"); +}); diff --git a/netwerk/test/unit/test_channel_priority.js b/netwerk/test/unit/test_channel_priority.js new file mode 100644 index 0000000000..74c144eb1c --- /dev/null +++ b/netwerk/test/unit/test_channel_priority.js @@ -0,0 +1,96 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +let httpserver; +let port; + +function startHttpServer() { + httpserver = new HttpServer(); + + httpserver.registerPathHandler("/resource", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + response.bodyOutputStream.write("data", 4); + }); + + httpserver.registerPathHandler("/redirect", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 302, "Redirect"); + response.setHeader("Location", "/resource", false); + response.setHeader("Cache-Control", "no-cache", false); + }); + + httpserver.start(-1); + port = httpserver.identity.primaryPort; +} + +function stopHttpServer() { + httpserver.stop(() => {}); +} + +function makeRequest(uri) { + let requestChannel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + requestChannel.QueryInterface(Ci.nsISupportsPriority); + requestChannel.priority = Ci.nsISupportsPriority.PRIORITY_HIGHEST; + requestChannel.asyncOpen(new ChannelListener(checkResponse, requestChannel)); +} + +function checkResponse(request, buffer, requestChannel) { + requestChannel.QueryInterface(Ci.nsISupportsPriority); + Assert.equal( + requestChannel.priority, + Ci.nsISupportsPriority.PRIORITY_HIGHEST + ); + + // the response channel can be different (if it was redirected) + let responseChannel = request.QueryInterface(Ci.nsISupportsPriority); + Assert.equal( + responseChannel.priority, + Ci.nsISupportsPriority.PRIORITY_HIGHEST + ); + + run_next_test(); +} + +add_test(function test_regular_request() { + makeRequest(`http://localhost:${port}/resource`); +}); + +add_test(function test_redirect() { + makeRequest(`http://localhost:${port}/redirect`); +}); + +function run_test() { + // jshint ignore:line + if (!runningInParent) { + // add a task to report test finished to parent process at the end of test queue, + // since do_register_cleanup is not available in child xpcshell test script. + add_test(function () { + do_send_remote_message("finished"); + run_next_test(); + }); + + // waiting for parent process to assign server port via configPort() + return; + } + + startHttpServer(); + registerCleanupFunction(stopHttpServer); + run_next_test(); +} + +// This is used by unit_ipc/test_channel_priority_wrap.js for e10s XPCShell test +/* exported configPort */ +function configPort(serverPort) { + // jshint ignore:line + port = serverPort; + run_next_test(); +} diff --git a/netwerk/test/unit/test_chunked_responses.js b/netwerk/test/unit/test_chunked_responses.js new file mode 100644 index 0000000000..502eb0179a --- /dev/null +++ b/netwerk/test/unit/test_chunked_responses.js @@ -0,0 +1,178 @@ +/* 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/. */ + +/* + * Test Chunked-Encoded response parsing. + */ + +//////////////////////////////////////////////////////////////////////////////// +// Test infrastructure + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var test_flags = []; +var testPathBase = "/chunked_hdrs"; + +function run_test() { + httpserver.start(-1); + + do_test_pending(); + run_test_number(1); +} + +function run_test_number(num) { + var testPath = testPathBase + num; + // eslint-disable-next-line no-eval + httpserver.registerPathHandler(testPath, eval("handler" + num)); + + var channel = setupChannel(testPath); + var flags = test_flags[num]; // OK if flags undefined for test + channel.asyncOpen( + // eslint-disable-next-line no-eval + new ChannelListener(eval("completeTest" + num), channel, flags) + ); +} + +function setupChannel(url) { + var chan = NetUtil.newChannel({ + uri: URL + url, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + return httpChan; +} + +function endTests() { + httpserver.stop(do_test_finished); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 1: FAIL because of overflowed chunked size. The parser uses long so +// the test case uses >64bit to fail on all platforms. +test_flags[1] = CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL; + +// eslint-disable-next-line no-unused-vars +function handler1(metadata, response) { + var body = "12345678123456789\r\ndata never reached"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +// eslint-disable-next-line no-unused-vars +function completeTest1(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_UNEXPECTED); + + run_test_number(2); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 2: FAIL because of non-hex in chunked length + +test_flags[2] = CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL; + +// eslint-disable-next-line no-unused-vars +function handler2(metadata, response) { + var body = "junkintheway 123\r\ndata never reached"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +// eslint-disable-next-line no-unused-vars +function completeTest2(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_UNEXPECTED); + run_test_number(3); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 3: OK in spite of non-hex digits after size in the length field + +test_flags[3] = CL_ALLOW_UNKNOWN_CL; + +// eslint-disable-next-line no-unused-vars +function handler3(metadata, response) { + var body = "c junkafter\r\ndata reached\r\n0\r\n\r\n"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +// eslint-disable-next-line no-unused-vars +function completeTest3(request, data, ctx) { + Assert.equal(request.status, 0); + run_test_number(4); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 4: Verify a fully compliant chunked response. + +test_flags[4] = CL_ALLOW_UNKNOWN_CL; + +// eslint-disable-next-line no-unused-vars +function handler4(metadata, response) { + var body = "c\r\ndata reached\r\n3\r\nhej\r\n0\r\n\r\n"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +// eslint-disable-next-line no-unused-vars +function completeTest4(request, data, ctx) { + Assert.equal(request.status, 0); + run_test_number(5); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 5: A chunk size larger than 32 bit but smaller than 64bit also fails +// This is probabaly subject to get improved at some point. + +test_flags[5] = CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL; + +// eslint-disable-next-line no-unused-vars +function handler5(metadata, response) { + var body = "123456781\r\ndata never reached"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +// eslint-disable-next-line no-unused-vars +function completeTest5(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_UNEXPECTED); + endTests(); + // run_test_number(6); +} diff --git a/netwerk/test/unit/test_coaleasing_h2_and_h3_connection.js b/netwerk/test/unit/test_coaleasing_h2_and_h3_connection.js new file mode 100644 index 0000000000..3c998ffe29 --- /dev/null +++ b/netwerk/test/unit/test_coaleasing_h2_and_h3_connection.js @@ -0,0 +1,109 @@ +/* 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"; + +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +let h2Port; +let h3Port; + +add_setup(async function setup() { + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + Services.prefs.setBoolPref("network.http.altsvc.oe", true); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); +}); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.http3.enable"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); + Services.prefs.clearUserPref("network.http.altsvc.oe"); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +add_task(async function testNotCoaleasingH2Connection() { + const host = "foo.example.com"; + Services.prefs.setCharPref("network.dns.localDomains", host); + + let server = new NodeHTTPSServer(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + + await server.execute(`global.h3Port = "${h3Port}";`); + await server.registerPathHandler("/altsvc", (req, resp) => { + const body = "done"; + resp.setHeader("Content-Length", body.length); + resp.setHeader("Alt-Svc", `h3-29=:${global.h3Port}`); + resp.writeHead(200); + resp.write(body); + resp.end(""); + }); + + let chan = makeChan(`https://${host}:${server.port()}/altsvc`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "http/1.1"); + + // Some delay to make sure the H3 speculative connection is created. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // To clear the altsvc cache. + Services.obs.notifyObservers(null, "last-pb-context-exited"); + + // Add another alt-svc header to route to moz-http2.js. + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + `${host};h2=:${h2Port}` + ); + + let start = new Date().getTime(); + chan = makeChan(`https://${host}:${server.port()}/server-timing`); + chan.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true; + [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + + // The time this request takes should be way more less than the + // neqo idle timeout (30s). + let duration = (new Date().getTime() - start) / 1000; + Assert.less(duration, 10); +}); diff --git a/netwerk/test/unit/test_compareURIs.js b/netwerk/test/unit/test_compareURIs.js new file mode 100644 index 0000000000..e460de1c29 --- /dev/null +++ b/netwerk/test/unit/test_compareURIs.js @@ -0,0 +1,61 @@ +"use strict"; + +function do_info(text, stack) { + if (!stack) { + stack = Components.stack.caller; + } + + dump( + "TEST-INFO | " + + stack.filename + + " | [" + + stack.name + + " : " + + stack.lineNumber + + "] " + + text + + "\n" + ); +} +function run_test() { + var tests = [ + ["http://mozilla.org/", "http://mozilla.org/somewhere/there", true], + ["http://mozilla.org/", "http://www.mozilla.org/", false], + ["http://mozilla.org/", "http://mozilla.org:80", true], + ["http://mozilla.org/", "http://mozilla.org:90", false], + ["http://mozilla.org", "https://mozilla.org", false], + ["http://mozilla.org", "https://mozilla.org:80", false], + ["http://mozilla.org:443", "https://mozilla.org", false], + ["https://mozilla.org:443", "https://mozilla.org", true], + ["https://mozilla.org:443", "https://mozilla.org/somewhere/", true], + ["about:", "about:", false], + ["data:text/plain,text", "data:text/plain,text", false], + ["about:blank", "about:blank", false], + ["about:", "http://mozilla.org/", false], + ["about:", "about:config", false], + ["about:text/plain,text", "data:text/plain,text", false], + ["jar:http://mozilla.org/!/", "http://mozilla.org/", true], + ["view-source:http://mozilla.org/", "http://mozilla.org/", true], + ]; + + tests.forEach(function (aTest) { + do_info("Comparing " + aTest[0] + " to " + aTest[1]); + + var uri1 = NetUtil.newURI(aTest[0]); + var uri2 = NetUtil.newURI(aTest[1]); + + var equal; + try { + Services.scriptSecurityManager.checkSameOriginURI( + uri1, + uri2, + false, + false + ); + equal = true; + } catch (e) { + equal = false; + } + Assert.equal(equal, aTest[2]); + }); +} diff --git a/netwerk/test/unit/test_compressappend.js b/netwerk/test/unit/test_compressappend.js new file mode 100644 index 0000000000..05f19be4b5 --- /dev/null +++ b/netwerk/test/unit/test_compressappend.js @@ -0,0 +1,99 @@ +// +// Test that data can be appended to a cache entry even when the data is +// compressed by the cache compression feature - bug 648429. +// + +"use strict"; + +function write_and_check(str, data, len) { + var written = str.write(data, len); + if (written != len) { + do_throw( + "str.write has not written all data!\n" + + " Expected: " + + len + + "\n" + + " Actual: " + + written + + "\n" + ); + } +} + +function TestAppend(compress, callback) { + this._compress = compress; + this._callback = callback; + this.run(); +} + +TestAppend.prototype = { + _compress: false, + _callback: null, + + run() { + evict_cache_entries(); + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + this.writeData.bind(this) + ); + }, + + writeData(status, entry) { + Assert.equal(status, Cr.NS_OK); + if (this._compress) { + entry.setMetaDataElement("uncompressed-len", "0"); + } + var os = entry.openOutputStream(0, 5); + write_and_check(os, "12345", 5); + os.close(); + entry.close(); + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + this.appendData.bind(this) + ); + }, + + appendData(status, entry) { + Assert.equal(status, Cr.NS_OK); + var os = entry.openOutputStream(entry.storageDataSize, 5); + write_and_check(os, "abcde", 5); + os.close(); + entry.close(); + + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + this.checkData.bind(this) + ); + }, + + checkData(status, entry) { + Assert.equal(status, Cr.NS_OK); + var self = this; + pumpReadStream(entry.openInputStream(0), function (str) { + Assert.equal(str.length, 10); + Assert.equal(str, "12345abcde"); + entry.close(); + + executeSoon(self._callback); + }); + }, +}; + +function run_test() { + do_get_profile(); + new TestAppend(false, run_test2); + do_test_pending(); +} + +function run_test2() { + new TestAppend(true, do_test_finished); +} diff --git a/netwerk/test/unit/test_connection_based_auth.js b/netwerk/test/unit/test_connection_based_auth.js new file mode 100644 index 0000000000..3a21ffcb77 --- /dev/null +++ b/netwerk/test/unit/test_connection_based_auth.js @@ -0,0 +1,91 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +add_task(async function test_connection_based_auth() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + + await proxy.registerConnectHandler((req, clientSocket, head) => { + if (!req.headers["proxy-authorization"]) { + clientSocket.write( + "HTTP/1.1 407 Unauthorized\r\n" + + "Proxy-agent: Node.js-Proxy\r\n" + + "Connection: keep-alive\r\n" + + "Proxy-Authenticate: mock_auth\r\n" + + "Content-Length: 0\r\n" + + "\r\n" + ); + + clientSocket.on("data", data => { + let array = data.toString().split("\r\n"); + let proxyAuthorization = ""; + for (let line of array) { + let pair = line.split(":").map(element => element.trim()); + if (pair[0] === "Proxy-Authorization") { + proxyAuthorization = pair[1]; + } + } + + if (proxyAuthorization === "moz_test_credentials") { + // We don't return 200 OK here, because we don't have a server + // to connect to. + clientSocket.write( + "HTTP/1.1 404 Not Found\r\nProxy-agent: Node.js-Proxy\r\n\r\n" + ); + } else { + clientSocket.write( + "HTTP/1.1 502 Error\r\nProxy-agent: Node.js-Proxy\r\n\r\n" + ); + } + clientSocket.destroy(); + }); + return; + } + + // We should not reach here. + clientSocket.write( + "HTTP/1.1 502 Error\r\nProxy-agent: Node.js-Proxy\r\n\r\n" + ); + clientSocket.destroy(); + }); + + let chan = makeChan(`https://example.ntlm.com/test`); + let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE); + Assert.equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST); + req.QueryInterface(Ci.nsIProxiedChannel); + Assert.equal(req.httpProxyConnectResponseCode, 404); + + await proxy.stop(); +}); diff --git a/netwerk/test/unit/test_content_encoding_gzip.js b/netwerk/test/unit/test_content_encoding_gzip.js new file mode 100644 index 0000000000..c4c342f155 --- /dev/null +++ b/netwerk/test/unit/test_content_encoding_gzip.js @@ -0,0 +1,126 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + { + url: "/test/cegzip1", + flags: CL_EXPECT_GZIP, + ce: "gzip", + body: [ + 0x1f, 0x8b, 0x08, 0x08, 0x5a, 0xa0, 0x31, 0x4f, 0x00, 0x03, 0x74, 0x78, + 0x74, 0x00, 0x2b, 0xc9, 0xc8, 0x2c, 0x56, 0x00, 0xa2, 0x92, 0xd4, 0xe2, + 0x12, 0x43, 0x2e, 0x00, 0xb9, 0x23, 0xd7, 0x3b, 0x0e, 0x00, 0x00, 0x00, + ], + datalen: 14, // the data length of the uncompressed document + }, + + { + url: "/test/cegzip2", + flags: CL_EXPECT_GZIP, + ce: "gzip, gzip", + body: [ + 0x1f, 0x8b, 0x08, 0x00, 0x72, 0xa1, 0x31, 0x4f, 0x00, 0x03, 0x93, 0xef, + 0xe6, 0xe0, 0x88, 0x5a, 0x60, 0xe8, 0xcf, 0xc0, 0x5c, 0x52, 0x51, 0xc2, + 0xa0, 0x7d, 0xf2, 0x84, 0x4e, 0x18, 0xc3, 0xa2, 0x49, 0x57, 0x1e, 0x09, + 0x39, 0xeb, 0x31, 0xec, 0x54, 0xbe, 0x6e, 0xcd, 0xc7, 0xc0, 0xc0, 0x00, + 0x00, 0x6e, 0x90, 0x7a, 0x85, 0x24, 0x00, 0x00, 0x00, + ], + datalen: 14, // the data length of the uncompressed document + }, + + { + url: "/test/cebrotli1", + flags: CL_EXPECT_GZIP, + ce: "br", + body: [0x0b, 0x02, 0x80, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x03], + + datalen: 5, // the data length of the uncompressed document + }, + + // this is not a brotli document + { + url: "/test/cebrotli2", + flags: CL_EXPECT_GZIP | CL_EXPECT_FAILURE, + ce: "br", + body: [0x0b, 0x0a, 0x09], + datalen: 3, + }, + + // this is brotli but should come through as identity due to prefs + { + url: "/test/cebrotli3", + flags: 0, + ce: "br", + body: [0x0b, 0x02, 0x80, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x03], + + datalen: 9, + }, +]; + +function setupChannel(url) { + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + url, + loadUsingSystemPrincipal: true, + }); +} + +function startIter() { + if (tests[index].url === "/test/cebrotli3") { + // this test wants to make sure we don't do brotli when not in a-e + prefs.setCharPref("network.http.accept-encoding", "gzip, deflate"); + } + var channel = setupChannel(tests[index].url); + channel.asyncOpen( + new ChannelListener(completeIter, channel, tests[index].flags) + ); +} + +function completeIter(request, data, ctx) { + if (!(tests[index].flags & CL_EXPECT_FAILURE)) { + Assert.equal(data.length, tests[index].datalen); + } + if (++index < tests.length) { + startIter(); + } else { + httpserver.stop(do_test_finished); + prefs.setCharPref("network.http.accept-encoding", cePref); + } +} + +var prefs; +var cePref; +function run_test() { + prefs = Services.prefs; + cePref = prefs.getCharPref("network.http.accept-encoding"); + prefs.setCharPref("network.http.accept-encoding", "gzip, deflate, br"); + prefs.setBoolPref("network.http.encoding.trustworthy_is_https", false); + + httpserver.registerPathHandler("/test/cegzip1", handler); + httpserver.registerPathHandler("/test/cegzip2", handler); + httpserver.registerPathHandler("/test/cebrotli1", handler); + httpserver.registerPathHandler("/test/cebrotli2", handler); + httpserver.registerPathHandler("/test/cebrotli3", handler); + httpserver.start(-1); + + startIter(); + do_test_pending(); +} + +function handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Encoding", tests[index].ce, false); + response.setHeader("Content-Length", "" + tests[index].body.length, false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + bos.setOutputStream(response.bodyOutputStream); + + response.processAsync(); + bos.writeByteArray(tests[index].body); + response.finish(); +} diff --git a/netwerk/test/unit/test_content_length_underrun.js b/netwerk/test/unit/test_content_length_underrun.js new file mode 100644 index 0000000000..a56b9ab1e1 --- /dev/null +++ b/netwerk/test/unit/test_content_length_underrun.js @@ -0,0 +1,293 @@ +/* + * Test Content-Length underrun behavior + */ + +//////////////////////////////////////////////////////////////////////////////// +// Test infrastructure + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var test_flags = []; +var testPathBase = "/cl_hdrs"; + +var prefs; +var enforcePrefStrict; +var enforcePrefSoft; +var enforcePrefStrictChunked; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +function run_test() { + prefs = Services.prefs; + enforcePrefStrict = prefs.getBoolPref("network.http.enforce-framing.http1"); + enforcePrefSoft = prefs.getBoolPref("network.http.enforce-framing.soft"); + enforcePrefStrictChunked = prefs.getBoolPref( + "network.http.enforce-framing.strict_chunked_encoding" + ); + + prefs.setBoolPref("network.http.enforce-framing.http1", true); + + httpserver.start(-1); + + do_test_pending(); + run_test_number(1); +} + +function run_test_number(num) { + let testPath = testPathBase + num; + // eslint-disable-next-line no-eval + httpserver.registerPathHandler(testPath, eval("handler" + num)); + + var channel = setupChannel(testPath); + let flags = test_flags[num]; // OK if flags undefined for test + channel.asyncOpen( + // eslint-disable-next-line no-eval + new ChannelListener(eval("completeTest" + num), channel, flags) + ); +} + +function run_gzip_test(num) { + let testPath = testPathBase + num; + // eslint-disable-next-line no-eval + httpserver.registerPathHandler(testPath, eval("handler" + num)); + + var channel = setupChannel(testPath); + + function StreamListener() {} + + StreamListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(aRequest) {}, + + onStopRequest(aRequest, aStatusCode) { + // Make sure we catch the error NS_ERROR_NET_PARTIAL_TRANSFER here. + Assert.equal(aStatusCode, Cr.NS_ERROR_NET_PARTIAL_TRANSFER); + // do_test_finished(); + endTests(); + }, + + onDataAvailable(request, stream, offset, count) {}, + }; + + let listener = new StreamListener(); + + channel.asyncOpen(listener); +} + +function setupChannel(url) { + var chan = NetUtil.newChannel({ + uri: URL + url, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + return httpChan; +} + +function endTests() { + // restore the prefs to pre-test values + prefs.setBoolPref("network.http.enforce-framing.http1", enforcePrefStrict); + prefs.setBoolPref("network.http.enforce-framing.soft", enforcePrefSoft); + prefs.setBoolPref( + "network.http.enforce-framing.strict_chunked_encoding", + enforcePrefStrictChunked + ); + httpserver.stop(do_test_finished); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 1: FAIL because of Content-Length underrun with HTTP 1.1 +test_flags[1] = CL_EXPECT_LATE_FAILURE; + +// eslint-disable-next-line no-unused-vars +function handler1(metadata, response) { + var body = "blablabla"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 556677\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +// eslint-disable-next-line no-unused-vars +function completeTest1(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_NET_PARTIAL_TRANSFER); + + run_test_number(11); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 11: PASS because of Content-Length underrun with HTTP 1.1 but non 2xx +test_flags[11] = CL_IGNORE_CL; + +// eslint-disable-next-line no-unused-vars +function handler11(metadata, response) { + var body = "blablabla"; + + response.seizePower(); + response.write("HTTP/1.1 404 NotOK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 556677\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +// eslint-disable-next-line no-unused-vars +function completeTest11(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + run_test_number(2); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 2: Succeed because Content-Length underrun is with HTTP 1.0 + +test_flags[2] = CL_IGNORE_CL; + +// eslint-disable-next-line no-unused-vars +function handler2(metadata, response) { + var body = "short content"; + + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 12345678\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +// eslint-disable-next-line no-unused-vars +function completeTest2(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + + // test 3 requires the enforce-framing prefs to be false + prefs.setBoolPref("network.http.enforce-framing.http1", false); + prefs.setBoolPref("network.http.enforce-framing.soft", false); + prefs.setBoolPref( + "network.http.enforce-framing.strict_chunked_encoding", + false + ); + run_test_number(3); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 3: SUCCEED with bad Content-Length because pref allows it +test_flags[3] = CL_IGNORE_CL; + +// eslint-disable-next-line no-unused-vars +function handler3(metadata, response) { + var body = "blablabla"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 556677\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +// eslint-disable-next-line no-unused-vars +function completeTest3(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + prefs.setBoolPref("network.http.enforce-framing.soft", true); + run_test_number(4); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 4: Succeed because a cut off deflate stream can't be detected +test_flags[4] = CL_IGNORE_CL; + +// eslint-disable-next-line no-unused-vars +function handler4(metadata, response) { + // this is the beginning of a deflate compressed response body + + var body = + "\xcd\x57\xcd\x6e\x1b\x37\x10\xbe\x07\xc8\x3b\x0c\x36\x68\x72\xd1" + + "\xbf\x92\x22\xb1\x57\x0a\x64\x4b\x6a\x0c\x28\xb6\x61\xa9\x41\x73" + + "\x2a\xb8\xbb\x94\x44\x98\xfb\x03\x92\x92\xec\x06\x7d\x97\x1e\xeb" + + "\xbe\x86\x5e\xac\xc3\x25\x97\xa2\x64\xb9\x75\x0b\x14\xe8\x69\x87" + + "\x33\x9c\x1f\x7e\x33\x9c\xe1\x86\x9f\x66\x9f\x27\xfd\x97\x2f\x20" + + "\xfc\x34\x1a\x0c\x35\x01\xa1\x62\x8a\xd3\xfe\xf5\xcd\xd5\xe5\xd5" + + "\x6c\x54\x83\x49\xbe\x60\x31\xa3\x1c\x12\x0a\x0b\x2a\x15\xcb\x33" + + "\x4d\xae\x19\x05\x19\xe7\x9c\x30\x41\x1b\x61\xd3\x28\x95\xfa\x29" + + "\x55\x04\x32\x92\xd2\x5e\x90\x50\x19\x0b\x56\x68\x9d\x00\xe2\x3c" + + "\x53\x34\x53\xbd\xc0\x99\x56\xf9\x4a\x51\xe0\x64\xcf\x18\x24\x24" + + "\x93\xb0\xca\x40\xd2\x15\x07\x6e\xbd\x37\x60\x82\x3b\x8f\x86\x22" + + "\x21\xcb\x15\x95\x35\x20\x91\xa4\x59\xac\xa9\x62\x95\x31\xed\x14" + + "\xc9\x98\x2c\x19\x15\x3a\x62\x45\xef\x70\x1b\x50\x05\xa4\x28\xc4" + + "\xf6\x21\x66\xa4\xdc\x83\x32\x09\x85\xc8\xe7\x54\xa2\x4b\x81\x74" + + "\xbe\x12\xc0\x91\xb9\x7d\x50\x24\xe2\x0c\xd9\x29\x06\x2e\xdd\x79"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 553677\r\n"); + response.write("Content-Encoding: deflate\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +// eslint-disable-next-line no-unused-vars +function completeTest4(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + + prefs.setBoolPref("network.http.enforce-framing.http1", true); + run_gzip_test(99); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 99: FAIL because a cut off gzip stream CAN be detected + +// Note that test 99 here is run completely different than the other tests in +// this file so if you add more tests here, consider adding them before this. + +// eslint-disable-next-line no-unused-vars +function handler99(metadata, response) { + // this is the beginning of a gzip compressed response body + + var body = + "\x1f\x8b\x08\x00\x80\xb9\x25\x53\x00\x03\xd4\xd9\x79\xb8\x8e\xe5" + + "\xba\x00\xf0\x65\x19\x33\x24\x15\x29\xf3\x50\x52\xc6\xac\x85\x10" + + "\x8b\x12\x22\x45\xe6\xb6\x21\x9a\x96\x84\x4c\x69\x32\xec\x84\x92" + + "\xcc\x99\x6a\xd9\x32\xa5\xd0\x40\xd9\xc6\x14\x15\x95\x28\x62\x9b" + + "\x09\xc9\x70\x4a\x25\x53\xec\x8e\x9c\xe5\x1c\x9d\xeb\xfe\x9d\x73" + + "\x9d\x3f\xf6\x1f\xe7\xbd\xae\xcf\xf3\xbd\xbf\xef\x7e\x9f\xeb\x79" + + "\xef\xf7\x99\xde\xe5\xee\x6e\xdd\x3b\x75\xeb\xd1\xb5\x6c\xb3\xd4" + + "\x47\x1f\x48\xf8\x17\x1d\x15\xce\x1d\x55\x92\x93\xcf\x97\xe7\x8e" + + "\x8b\xca\xe4\xca\x55\x92\x2a\x54\x4e\x4e\x4e\x4a\xa8\x78\x53\xa5" + + "\x8a\x15\x2b\x55\x4a\xfa\xe3\x7b\x85\x8a\x37\x55\x48\xae\x92\x50" + + "\xb4\xc2\xbf\xaa\x41\x17\x1f\xbd\x7b\xf6\xba\xaf\x47\xd1\xa2\x09" + + "\x3d\xba\x75\xeb\xf5\x3f\xc5\xfd\x6f\xbf\xff\x3f\x3d\xfa\xd7\x6d" + + "\x74\x7b\x62\x86\x0c\xff\x79\x9e\x98\x50\x33\xe1\x8f\xb3\x01\xef" + + "\xb6\x38\x7f\x9e\x92\xee\xf9\xa7\xee\xcb\x74\x21\x26\x25\xa1\x6a" + + "\x42\xf6\x73\xff\x96\x4c\x28\x91\x90\xe5\xdc\x79\xa6\x8b\xe2\x52" + + "\xd2\xbf\x5d\x28\x2b\x24\x26\xfc\xa9\xcc\x96\x1e\x97\x31\xfd\xba" + + "\xee\xe9\xde\x3d\x31\xe5\x4f\x65\xc1\xf4\xb8\x0b\x65\x86\x8b\xca"; + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 553677\r\n"); + response.write("Content-Encoding: gzip\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} diff --git a/netwerk/test/unit/test_content_sniffer.js b/netwerk/test/unit/test_content_sniffer.js new file mode 100644 index 0000000000..6299c90ae5 --- /dev/null +++ b/netwerk/test/unit/test_content_sniffer.js @@ -0,0 +1,155 @@ +// This file tests nsIContentSniffer, introduced in bug 324985 + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const unknownType = "application/x-unknown-content-type"; +const sniffedType = "application/x-sniffed"; + +const snifferCID = Components.ID("{4c93d2db-8a56-48d7-b261-9cf2a8d998eb}"); +const snifferContract = "@mozilla.org/network/unittest/contentsniffer;1"; +const categoryName = "net-content-sniffers"; + +var sniffing_enabled = true; + +var isNosniff = false; + +/** + * This object is both a factory and an nsIContentSniffer implementation (so, it + * is de-facto a service) + */ +var sniffer = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIContentSniffer"]), + createInstance: function sniffer_ci(iid) { + return this.QueryInterface(iid); + }, + + getMIMETypeFromContent(request, data, length) { + return sniffedType; + }, +}; + +var listener = { + onStartRequest: function test_onStartR(request) { + try { + var chan = request.QueryInterface(Ci.nsIChannel); + if (chan.contentType == unknownType) { + do_throw("Type should not be unknown!"); + } + if (isNosniff) { + if (chan.contentType == sniffedType) { + do_throw("Sniffer called for X-Content-Type-Options:nosniff"); + } + } else if ( + sniffing_enabled && + this._iteration > 2 && + chan.contentType != sniffedType + ) { + do_throw( + "Expecting <" + + sniffedType + + "> but got <" + + chan.contentType + + "> for " + + chan.URI.spec + ); + } else if (!sniffing_enabled && chan.contentType == sniffedType) { + do_throw( + "Sniffing not enabled but sniffer called for " + chan.URI.spec + ); + } + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable: function test_ODA() { + throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); + }, + + onStopRequest: function test_onStopR(request, status) { + run_test_iteration(this._iteration); + do_test_finished(); + }, + + _iteration: 1, +}; + +function makeChan(url) { + var chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); + if (sniffing_enabled) { + chan.loadFlags |= Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS; + } + + return chan; +} + +var httpserv = null; +var urls = null; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/nosniff", nosniffHandler); + httpserv.start(-1); + + urls = [ + // NOTE: First URL here runs without our content sniffer + "data:" + unknownType + ", Some text", + "data:" + unknownType + ", Text", // Make sure sniffing works even if we + // used the unknown content sniffer too + "data:text/plain, Some more text", + "http://localhost:" + httpserv.identity.primaryPort, + "http://localhost:" + httpserv.identity.primaryPort + "/nosniff", + ]; + + Components.manager.nsIComponentRegistrar.registerFactory( + snifferCID, + "Unit test content sniffer", + snifferContract, + sniffer + ); + + run_test_iteration(1); +} + +function nosniffHandler(request, response) { + response.setHeader("X-Content-Type-Options", "nosniff"); +} + +function run_test_iteration(index) { + if (index > urls.length) { + if (sniffing_enabled) { + sniffing_enabled = false; + index = listener._iteration = 1; + } else { + do_test_pending(); + httpserv.stop(do_test_finished); + return; // we're done + } + } + + if (sniffing_enabled && index == 2) { + // Register our sniffer only here + // This also makes sure that dynamic registration is working + var catMan = Services.catMan; + catMan.nsICategoryManager.addCategoryEntry( + categoryName, + "unit test", + snifferContract, + false, + true + ); + } else if (sniffing_enabled && index == 5) { + isNosniff = true; + } + + var chan = makeChan(urls[index - 1]); + + listener._iteration++; + chan.asyncOpen(listener); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cookie_blacklist.js b/netwerk/test/unit/test_cookie_blacklist.js new file mode 100644 index 0000000000..d6d25927e9 --- /dev/null +++ b/netwerk/test/unit/test_cookie_blacklist.js @@ -0,0 +1,43 @@ +"use strict"; + +const GOOD_COOKIE = "GoodCookie=OMNOMNOM"; +const SPACEY_COOKIE = "Spacey Cookie=Major Tom"; + +add_task(async () => { + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + Services.prefs.setBoolPref("dom.security.https_first", false); + + var cookieURI = Services.io.newURI( + "http://mozilla.org/test_cookie_blacklist.js" + ); + const channel = NetUtil.newChannel({ + uri: cookieURI, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + Services.cookies.setCookieStringFromHttp( + cookieURI, + "BadCookie1=\x01", + channel + ); + Services.cookies.setCookieStringFromHttp(cookieURI, "BadCookie2=\v", channel); + Services.cookies.setCookieStringFromHttp( + cookieURI, + "Bad\x07Name=illegal", + channel + ); + Services.cookies.setCookieStringFromHttp(cookieURI, GOOD_COOKIE, channel); + Services.cookies.setCookieStringFromHttp(cookieURI, SPACEY_COOKIE, channel); + + CookieXPCShellUtils.createServer({ hosts: ["mozilla.org"] }); + + const storedCookie = await CookieXPCShellUtils.getCookieStringFromDocument( + cookieURI.spec + ); + Assert.equal(storedCookie, GOOD_COOKIE + "; " + SPACEY_COOKIE); + Services.prefs.clearUserPref("dom.security.https_first"); +}); diff --git a/netwerk/test/unit/test_cookie_header.js b/netwerk/test/unit/test_cookie_header.js new file mode 100644 index 0000000000..e8c86849f3 --- /dev/null +++ b/netwerk/test/unit/test_cookie_header.js @@ -0,0 +1,110 @@ +// This file tests bug 250375 + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserv.identity.primaryPort + "/"; +}); + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +function check_request_header(chan, name, value) { + var chanValue; + try { + chanValue = chan.getRequestHeader(name); + } catch (e) { + do_throw( + "Expected to find header '" + + name + + "' but didn't find it, got exception: " + + e + ); + } + dump("Value for header '" + name + "' is '" + chanValue + "'\n"); + Assert.equal(chanValue, value); +} + +var cookieVal = "C1=V1"; + +var listener = { + onStartRequest: function test_onStartR(request) { + try { + var chan = request.QueryInterface(Ci.nsIHttpChannel); + check_request_header(chan, "Cookie", cookieVal); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable: function test_ODA() { + throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); + }, + + onStopRequest: async function test_onStopR(request, status) { + if (this._iteration == 1) { + await run_test_continued(); + } else { + do_test_pending(); + httpserv.stop(do_test_finished); + } + do_test_finished(); + }, + + _iteration: 1, +}; + +function makeChan() { + return NetUtil.newChannel({ + uri: URL, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var httpserv = null; + +function run_test() { + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + httpserv = new HttpServer(); + httpserv.start(-1); + + var chan = makeChan(); + + chan.setRequestHeader("Cookie", cookieVal, false); + + chan.asyncOpen(listener); + + do_test_pending(); +} + +async function run_test_continued() { + var chan = makeChan(); + + var cookie2 = "C2=V2"; + + await CookieXPCShellUtils.setCookieToDocument(chan.URI.spec, cookie2); + + chan.setRequestHeader("Cookie", cookieVal, false); + + // We expect that the setRequestHeader overrides the + // automatically-added one, so insert cookie2 in front + cookieVal = cookie2 + "; " + cookieVal; + + listener._iteration++; + chan.asyncOpen(listener); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cookie_ipv6.js b/netwerk/test/unit/test_cookie_ipv6.js new file mode 100644 index 0000000000..38b35c2a9e --- /dev/null +++ b/netwerk/test/unit/test_cookie_ipv6.js @@ -0,0 +1,51 @@ +/* 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/. */ + +/* + * Test that channels with different LoadInfo + * are stored in separate namespaces ("cookie jars") + */ + +"use strict"; + +let ip = "[::1]"; +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return `http://${ip}:${httpserver.identity.primaryPort}/`; +}); + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +let httpserver = new HttpServer(); + +function cookieSetHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader( + "Set-Cookie", + `Set-Cookie: T1=T2; path=/; SameSite=Lax; domain=${ip}; httponly`, + false + ); + response.setHeader("Content-Type", "text/html"); + response.setHeader("Content-Length", "2"); + response.bodyOutputStream.write("Ok", "Ok".length); +} + +add_task(async function test_cookie_ipv6() { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + httpserver.registerPathHandler("/", cookieSetHandler); + httpserver._start(-1, ip); + + var chan = NetUtil.newChannel({ + uri: URL, + loadUsingSystemPrincipal: true, + }); + await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve)); + }); + equal(Services.cookies.cookies.length, 1); +}); diff --git a/netwerk/test/unit/test_cookiejars.js b/netwerk/test/unit/test_cookiejars.js new file mode 100644 index 0000000000..1b9719eb0f --- /dev/null +++ b/netwerk/test/unit/test_cookiejars.js @@ -0,0 +1,186 @@ +/* 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/. */ + +/* + * Test that channels with different LoadInfo + * are stored in separate namespaces ("cookie jars") + */ + +"use strict"; + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); + +var cookieSetPath = "/setcookie"; +var cookieCheckPath = "/checkcookie"; + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +// Test array: +// - element 0: name for cookie, used both to set and later to check +// - element 1: loadInfo (determines cookie namespace) +// +// TODO: bug 722850: make private browsing work per-app, and add tests. For now +// all values are 'false' for PB. + +var tests = [ + { + cookieName: "LCC_App0_BrowF_PrivF", + originAttributes: new OriginAttributes(0, false, 0), + }, + { + cookieName: "LCC_App0_BrowT_PrivF", + originAttributes: new OriginAttributes(0, true, 0), + }, + { + cookieName: "LCC_App1_BrowF_PrivF", + originAttributes: new OriginAttributes(1, false, 0), + }, + { + cookieName: "LCC_App1_BrowT_PrivF", + originAttributes: new OriginAttributes(1, true, 0), + }, +]; + +// test number: index into 'tests' array +var i = 0; + +function setupChannel(path) { + var chan = NetUtil.newChannel({ + uri: URL + path, + loadUsingSystemPrincipal: true, + }); + chan.loadInfo.originAttributes = tests[i].originAttributes; + chan.QueryInterface(Ci.nsIHttpChannel); + + let loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + + if (chan.loadInfo.originAttributes.privateBrowsingId == 0) { + loadGroup.notificationCallbacks = Cu.createLoadContext(); + chan.loadGroup = loadGroup; + + chan.notificationCallbacks = Cu.createLoadContext(); + } else { + loadGroup.notificationCallbacks = Cu.createPrivateLoadContext(); + chan.loadGroup = loadGroup; + + chan.notificationCallbacks = Cu.createPrivateLoadContext(); + } + + return chan; +} + +function setCookie() { + var channel = setupChannel(cookieSetPath); + channel.setRequestHeader("foo-set-cookie", tests[i].cookieName, false); + channel.asyncOpen(new ChannelListener(setNextCookie, null)); +} + +function setNextCookie(request, data, context) { + if (++i == tests.length) { + // all cookies set: switch to checking them + i = 0; + checkCookie(); + } else { + info("setNextCookie:i=" + i); + setCookie(); + } +} + +// Open channel that should send one and only one correct Cookie: header to +// server, corresponding to it's namespace +function checkCookie() { + var channel = setupChannel(cookieCheckPath); + channel.asyncOpen(new ChannelListener(completeCheckCookie, null)); +} + +function completeCheckCookie(request, data, context) { + // Look for all cookies in what the server saw: fail if we see any besides the + // one expected cookie for each namespace; + var expectedCookie = tests[i].cookieName; + request.QueryInterface(Ci.nsIHttpChannel); + var cookiesSeen = request.getResponseHeader("foo-saw-cookies"); + + var j; + for (j = 0; j < tests.length; j++) { + var cookieToCheck = tests[j].cookieName; + let found = cookiesSeen.includes(cookieToCheck); + if (found && expectedCookie != cookieToCheck) { + do_throw( + "test index " + + i + + ": found unexpected cookie '" + + cookieToCheck + + "': in '" + + cookiesSeen + + "'" + ); + } else if (!found && expectedCookie == cookieToCheck) { + do_throw( + "test index " + + i + + ": missing expected cookie '" + + expectedCookie + + "': in '" + + cookiesSeen + + "'" + ); + } + } + // If we get here we're good. + info("Saw only correct cookie '" + expectedCookie + "'"); + Assert.ok(true); + + if (++i == tests.length) { + // end of tests + httpserver.stop(do_test_finished); + } else { + checkCookie(); + } +} + +function run_test() { + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + httpserver.registerPathHandler(cookieSetPath, cookieSetHandler); + httpserver.registerPathHandler(cookieCheckPath, cookieCheckHandler); + httpserver.start(-1); + + setCookie(); + do_test_pending(); +} + +function cookieSetHandler(metadata, response) { + var cookieName = metadata.getHeader("foo-set-cookie"); + + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Set-Cookie", cookieName + "=1; Path=/", false); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); +} + +function cookieCheckHandler(metadata, response) { + var cookies = metadata.getHeader("Cookie"); + + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("foo-saw-cookies", cookies, false); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); +} diff --git a/netwerk/test/unit/test_cookiejars_safebrowsing.js b/netwerk/test/unit/test_cookiejars_safebrowsing.js new file mode 100644 index 0000000000..19a07cf86b --- /dev/null +++ b/netwerk/test/unit/test_cookiejars_safebrowsing.js @@ -0,0 +1,229 @@ +/* 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/. */ + +/* + * Description of the test: + * We show that we can separate the safebrowsing cookie by creating a custom + * OriginAttributes using a unique safebrowsing first-party domain. Setting this + * custom OriginAttributes on the loadInfo of the channel allows us to query the + * first-party domain and therefore separate the safebrowsing cookie in its own + * cookie-jar. For testing safebrowsing update we do >> NOT << emulate a response + * in the body, rather we only set the cookies in the header of the response + * and confirm that cookies are separated in their own cookie-jar. + * + * 1) We init safebrowsing and simulate an update (cookies are set for localhost) + * + * 2) We open a channel that should send regular cookies, but not the + * safebrowsing cookie. + * + * 3) We open a channel with a custom callback, simulating a safebrowsing cookie + * that should send this simulated safebrowsing cookie as well as the + * real safebrowsing cookies. (Confirming that the safebrowsing cookies + * actually get stored in the correct jar). + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var setCookiePath = "/setcookie"; +var checkCookiePath = "/checkcookie"; +var safebrowsingUpdatePath = "/safebrowsingUpdate"; +var safebrowsingGethashPath = "/safebrowsingGethash"; +var httpserver; + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +function cookieSetHandler(metadata, response) { + var cookieName = metadata.getHeader("set-cookie"); + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("set-Cookie", cookieName + "=1; Path=/", false); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); +} + +function cookieCheckHandler(metadata, response) { + var cookies = metadata.getHeader("Cookie"); + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("saw-cookies", cookies, false); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); +} + +function safebrowsingUpdateHandler(metadata, response) { + var cookieName = "sb-update-cookie"; + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("set-Cookie", cookieName + "=1; Path=/", false); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); +} + +function safebrowsingGethashHandler(metadata, response) { + var cookieName = "sb-gethash-cookie"; + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("set-Cookie", cookieName + "=1; Path=/", false); + response.setHeader("Content-Type", "text/plain"); + + let msg = "test-phish-simplea:1:32\n" + "a".repeat(32); + response.bodyOutputStream.write(msg, msg.length); +} + +function setupChannel(path, originAttributes) { + var channel = NetUtil.newChannel({ + uri: URL + path, + loadUsingSystemPrincipal: true, + }); + channel.loadInfo.originAttributes = originAttributes; + channel.QueryInterface(Ci.nsIHttpChannel); + return channel; +} + +function run_test() { + // Set up a profile + do_get_profile(); + + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + httpserver = new HttpServer(); + httpserver.registerPathHandler(setCookiePath, cookieSetHandler); + httpserver.registerPathHandler(checkCookiePath, cookieCheckHandler); + httpserver.registerPathHandler( + safebrowsingUpdatePath, + safebrowsingUpdateHandler + ); + httpserver.registerPathHandler( + safebrowsingGethashPath, + safebrowsingGethashHandler + ); + + httpserver.start(-1); + run_next_test(); +} + +// this test does not emulate a response in the body, +// rather we only set the cookies in the header of response. +add_test(function test_safebrowsing_update() { + var streamUpdater = Cc[ + "@mozilla.org/url-classifier/streamupdater;1" + ].getService(Ci.nsIUrlClassifierStreamUpdater); + + function onSuccess() { + run_next_test(); + } + function onUpdateError() { + do_throw("ERROR: received onUpdateError!"); + } + function onDownloadError() { + do_throw("ERROR: received onDownloadError!"); + } + + streamUpdater.downloadUpdates( + "test-phish-simple,test-malware-simple", + "", + true, + URL + safebrowsingUpdatePath, + onSuccess, + onUpdateError, + onDownloadError + ); +}); + +add_test(function test_safebrowsing_gethash() { + var hashCompleter = Cc[ + "@mozilla.org/url-classifier/hashcompleter;1" + ].getService(Ci.nsIUrlClassifierHashCompleter); + + hashCompleter.complete( + "aaaa", + URL + safebrowsingGethashPath, + "test-phish-simple", + { + completionV2(hash, table, chunkId) {}, + + completionFinished(status) { + Assert.equal(status, Cr.NS_OK); + run_next_test(); + }, + } + ); +}); + +add_test(function test_non_safebrowsing_cookie() { + var cookieName = "regCookie_id0"; + var originAttributes = new OriginAttributes(0, false, 0); + + function setNonSafeBrowsingCookie() { + var channel = setupChannel(setCookiePath, originAttributes); + channel.setRequestHeader("set-cookie", cookieName, false); + channel.asyncOpen(new ChannelListener(checkNonSafeBrowsingCookie, null)); + } + + function checkNonSafeBrowsingCookie() { + var channel = setupChannel(checkCookiePath, originAttributes); + channel.asyncOpen( + new ChannelListener(completeCheckNonSafeBrowsingCookie, null) + ); + } + + function completeCheckNonSafeBrowsingCookie(request, data, context) { + // Confirm that only the >> ONE << cookie is sent over the channel. + var expectedCookie = cookieName + "=1"; + request.QueryInterface(Ci.nsIHttpChannel); + var cookiesSeen = request.getResponseHeader("saw-cookies"); + Assert.equal(cookiesSeen, expectedCookie); + run_next_test(); + } + + setNonSafeBrowsingCookie(); +}); + +add_test(function test_safebrowsing_cookie() { + var cookieName = "sbCookie_id4294967294"; + var originAttributes = new OriginAttributes(0, false, 0); + originAttributes.firstPartyDomain = + "safebrowsing.86868755-6b82-4842-b301-72671a0db32e.mozilla"; + + function setSafeBrowsingCookie() { + var channel = setupChannel(setCookiePath, originAttributes); + channel.setRequestHeader("set-cookie", cookieName, false); + channel.asyncOpen(new ChannelListener(checkSafeBrowsingCookie, null)); + } + + function checkSafeBrowsingCookie() { + var channel = setupChannel(checkCookiePath, originAttributes); + channel.asyncOpen( + new ChannelListener(completeCheckSafeBrowsingCookie, null) + ); + } + + function completeCheckSafeBrowsingCookie(request, data, context) { + // Confirm that all >> THREE << cookies are sent back over the channel: + // a) the safebrowsing cookie set when updating + // b) the safebrowsing cookie set when sending gethash + // c) the regular cookie with custom loadcontext defined in this test. + var expectedCookies = "sb-update-cookie=1; "; + expectedCookies += "sb-gethash-cookie=1; "; + expectedCookies += cookieName + "=1"; + request.QueryInterface(Ci.nsIHttpChannel); + var cookiesSeen = request.getResponseHeader("saw-cookies"); + + Assert.equal(cookiesSeen, expectedCookies); + httpserver.stop(do_test_finished); + } + + setSafeBrowsingCookie(); +}); diff --git a/netwerk/test/unit/test_cookies_async_failure.js b/netwerk/test/unit/test_cookies_async_failure.js new file mode 100644 index 0000000000..2ae50bf163 --- /dev/null +++ b/netwerk/test/unit/test_cookies_async_failure.js @@ -0,0 +1,514 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the various ways opening a cookie database can fail in an asynchronous +// (i.e. after synchronous initialization) manner, and that the database is +// renamed and recreated under each circumstance. These circumstances are, in no +// particular order: +// +// 1) A write operation failing after the database has been read in. +// 2) Asynchronous read failure due to a corrupt database. +// 3) Synchronous read failure due to a corrupt database, when reading: +// a) a single base domain; +// b) the entire database. +// 4) Asynchronous read failure, followed by another failure during INSERT but +// before the database closes for rebuilding. (The additional error should be +// ignored.) +// 5) Asynchronous read failure, followed by an INSERT failure during rebuild. +// This should result in an abort of the database rebuild; the partially- +// built database should be moved to 'cookies.sqlite.bak-rebuild'. + +"use strict"; + +let profile; +let cookie; + +add_task(async () => { + // Set up a profile. + profile = do_get_profile(); + Services.prefs.setBoolPref("dom.security.https_first", false); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // Bug 1617611 - Fix all the tests broken by "cookies SameSite=Lax by default" + Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false); + + // The server. + const hosts = ["foo.com", "hither.com", "haithur.com", "bar.com"]; + for (let i = 0; i < 3000; ++i) { + hosts.push(i + ".com"); + } + CookieXPCShellUtils.createServer({ hosts }); + + // Get the cookie file and the backup file. + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); + + // Create a cookie object for testing. + let now = Date.now() * 1000; + let futureExpiry = Math.round(now / 1e6 + 1000); + cookie = new Cookie( + "oh", + "hai", + "bar.com", + "/", + futureExpiry, + now, + now, + false, + false, + false + ); + + await run_test_1(); + await run_test_2(); + await run_test_3(); + await run_test_4(); + await run_test_5(); + Services.prefs.clearUserPref("dom.security.https_first"); + Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); + +function do_get_backup_file(profile) { + let file = profile.clone(); + file.append("cookies.sqlite.bak"); + return file; +} + +function do_get_rebuild_backup_file(profile) { + let file = profile.clone(); + file.append("cookies.sqlite.bak-rebuild"); + return file; +} + +function do_corrupt_db(file) { + // Sanity check: the database size should be larger than 320k, since we've + // written about 460k of data. If it's not, let's make it obvious now. + let size = file.fileSize; + Assert.ok(size > 320e3); + + // Corrupt the database by writing bad data to the end of the file. We + // assume that the important metadata -- table structure etc -- is stored + // elsewhere, and that doing this will not cause synchronous failure when + // initializing the database connection. This is totally empirical -- + // overwriting between 1k and 100k of live data seems to work. (Note that the + // database file will be larger than the actual content requires, since the + // cookie service uses a large growth increment. So we calculate the offset + // based on the expected size of the content, not just the file size.) + let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init(file, 2, -1, 0); + let sstream = ostream.QueryInterface(Ci.nsISeekableStream); + let n = size - 320e3 + 20e3; + sstream.seek(Ci.nsISeekableStream.NS_SEEK_SET, size - n); + for (let i = 0; i < n; ++i) { + ostream.write("a", 1); + } + ostream.flush(); + ostream.close(); + + Assert.equal(file.clone().fileSize, size); + return size; +} + +async function run_test_1() { + // Load the profile and populate it. + await CookieXPCShellUtils.setCookieToDocument( + "http://foo.com/", + "oh=hai; max-age=1000" + ); + + // Close the profile. + await promise_close_profile(); + + // Open a database connection now, before we load the profile and begin + // asynchronous write operations. + let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12); + Assert.equal(do_count_cookies_in_db(db.db), 1); + + // Load the profile, and wait for async read completion... + await promise_load_profile(); + + // Insert a row. + db.insertCookie(cookie); + db.close(); + + // Attempt to insert a cookie with the same (name, host, path) triplet. + Services.cookies.add( + cookie.host, + cookie.path, + cookie.name, + "hallo", + cookie.isSecure, + cookie.isHttpOnly, + cookie.isSession, + cookie.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + + // Check that the cookie service accepted the new cookie. + Assert.equal(Services.cookies.countCookiesFromHost(cookie.host), 1); + + let isRebuildingDone = false; + let rebuildingObserve = function (subject, topic, data) { + isRebuildingDone = true; + Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding"); + }; + Services.obs.addObserver(rebuildingObserve, "cookie-db-rebuilding"); + + // Crash test: we're going to rebuild the cookie database. Close all the db + // connections in the main thread and initialize a new database file in the + // cookie thread. Trigger some access of cookies to ensure we won't crash in + // the chaos status. + for (let i = 0; i < 10; ++i) { + Assert.equal(Services.cookies.countCookiesFromHost(cookie.host), 1); + await new Promise(resolve => executeSoon(resolve)); + } + + // Wait for the cookie service to rename the old database and rebuild if not yet. + if (!isRebuildingDone) { + Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding"); + await new _promise_observer("cookie-db-rebuilding"); + } + + await new Promise(resolve => executeSoon(resolve)); + + // At this point, the cookies should still be in memory. + Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 1); + Assert.equal(Services.cookies.countCookiesFromHost(cookie.host), 1); + Assert.equal(do_count_cookies(), 2); + + // Close the profile. + await promise_close_profile(); + + // Check that the original database was renamed, and that it contains the + // original cookie. + Assert.ok(do_get_backup_file(profile).exists()); + let backupdb = Services.storage.openDatabase(do_get_backup_file(profile)); + Assert.equal(do_count_cookies_in_db(backupdb, "foo.com"), 1); + backupdb.close(); + + // Load the profile, and check that it contains the new cookie. + do_load_profile(); + + Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 1); + let cookies = Services.cookies.getCookiesFromHost(cookie.host, {}); + Assert.equal(cookies.length, 1); + let dbcookie = cookies[0]; + Assert.equal(dbcookie.value, "hallo"); + + // Close the profile. + await promise_close_profile(); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} + +async function run_test_2() { + // Load the profile and populate it. + do_load_profile(); + + Services.cookies.runInTransaction(_ => { + let uri = NetUtil.newURI("http://foo.com/"); + const channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + for (let i = 0; i < 3000; ++i) { + let uri = NetUtil.newURI("http://" + i + ".com/"); + Services.cookies.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + } + }); + + // Close the profile. + await promise_close_profile(); + + // Corrupt the database file. + let size = do_corrupt_db(do_get_cookie_file(profile)); + + // Load the profile. + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Recreate a new database since it was corrupted + Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + await promise_close_profile(); + + // Check that the original database was renamed. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + let db = Services.storage.openDatabase(do_get_cookie_file(profile)); + db.close(); + + do_load_profile(); + Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + await promise_close_profile(); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} + +async function run_test_3() { + // Set the maximum cookies per base domain limit to a large value, so that + // corrupting the database is easier. + Services.prefs.setIntPref("network.cookie.maxPerHost", 3000); + + // Load the profile and populate it. + do_load_profile(); + Services.cookies.runInTransaction(_ => { + let uri = NetUtil.newURI("http://hither.com/"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + for (let i = 0; i < 10; ++i) { + Services.cookies.setCookieStringFromHttp( + uri, + "oh" + i + "=hai; max-age=1000", + channel + ); + } + uri = NetUtil.newURI("http://haithur.com/"); + channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + for (let i = 10; i < 3000; ++i) { + Services.cookies.setCookieStringFromHttp( + uri, + "oh" + i + "=hai; max-age=1000", + channel + ); + } + }); + + // Close the profile. + await promise_close_profile(); + + // Corrupt the database file. + let size = do_corrupt_db(do_get_cookie_file(profile)); + + // Load the profile. + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Recreate a new database since it was corrupted + Assert.equal(Services.cookies.countCookiesFromHost("hither.com"), 0); + Assert.equal(Services.cookies.countCookiesFromHost("haithur.com"), 0); + + // Close the profile. + await promise_close_profile(); + + let db = Services.storage.openDatabase(do_get_cookie_file(profile)); + Assert.equal(do_count_cookies_in_db(db, "hither.com"), 0); + Assert.equal(do_count_cookies_in_db(db), 0); + db.close(); + + // Check that the original database was renamed. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + + // Rename it back, and try loading the entire database synchronously. + do_get_backup_file(profile).moveTo(null, "cookies.sqlite"); + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Synchronously read in everything. + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + await promise_close_profile(); + + db = Services.storage.openDatabase(do_get_cookie_file(profile)); + Assert.equal(do_count_cookies_in_db(db), 0); + db.close(); + + // Check that the original database was renamed. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} + +async function run_test_4() { + // Load the profile and populate it. + do_load_profile(); + Services.cookies.runInTransaction(_ => { + let uri = NetUtil.newURI("http://foo.com/"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + for (let i = 0; i < 3000; ++i) { + let uri = NetUtil.newURI("http://" + i + ".com/"); + Services.cookies.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + } + }); + + // Close the profile. + await promise_close_profile(); + + // Corrupt the database file. + let size = do_corrupt_db(do_get_cookie_file(profile)); + + // Load the profile. + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Recreate a new database since it was corrupted + Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0); + + // Queue up an INSERT for the same base domain. This should also go into + // memory and be written out during database rebuild. + await CookieXPCShellUtils.setCookieToDocument( + "http://0.com/", + "oh2=hai; max-age=1000" + ); + + // At this point, the cookies should still be in memory. + Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 1); + Assert.equal(do_count_cookies(), 1); + + // Close the profile. + await promise_close_profile(); + + // Check that the original database was renamed. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + + // Load the profile, and check that it contains the new cookie. + do_load_profile(); + Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 1); + Assert.equal(do_count_cookies(), 1); + + // Close the profile. + await promise_close_profile(); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} + +async function run_test_5() { + // Load the profile and populate it. + do_load_profile(); + Services.cookies.runInTransaction(_ => { + let uri = NetUtil.newURI("http://bar.com/"); + const channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + Services.cookies.setCookieStringFromHttp( + uri, + "oh=hai; path=/; max-age=1000", + channel + ); + for (let i = 0; i < 3000; ++i) { + let uri = NetUtil.newURI("http://" + i + ".com/"); + Services.cookies.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + } + }); + + // Close the profile. + await promise_close_profile(); + + // Corrupt the database file. + let size = do_corrupt_db(do_get_cookie_file(profile)); + + // Load the profile. + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Recreate a new database since it was corrupted + Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 0); + Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0); + Assert.equal(do_count_cookies(), 0); + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + Assert.ok(!do_get_rebuild_backup_file(profile).exists()); + + // Open a database connection, and write a row that will trigger a constraint + // violation. + let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12); + db.insertCookie(cookie); + Assert.equal(do_count_cookies_in_db(db.db, "bar.com"), 1); + Assert.equal(do_count_cookies_in_db(db.db), 1); + db.close(); + + // Check that the original backup and the database itself are gone. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + + Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 0); + Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. We do not need to wait for completion, because the + // database has already been closed. Ensure the cookie file is unlocked. + await promise_close_profile(); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} diff --git a/netwerk/test/unit/test_cookies_privatebrowsing.js b/netwerk/test/unit/test_cookies_privatebrowsing.js new file mode 100644 index 0000000000..9d3528440a --- /dev/null +++ b/netwerk/test/unit/test_cookies_privatebrowsing.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test private browsing mode. + +"use strict"; + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function getCookieStringFromPrivateDocument(uriSpec) { + return CookieXPCShellUtils.getCookieStringFromDocument(uriSpec, { + privateBrowsing: true, + }); +} + +add_task(async () => { + // Set up a profile. + do_get_profile(); + + // We don't want to have CookieJarSettings blocking this test. + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // Test with cookies enabled. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref("dom.security.https_first", false); + + // Test with https-first-mode disabled in PBM + Services.prefs.setBoolPref("dom.security.https_first_pbm", false); + + CookieXPCShellUtils.createServer({ hosts: ["foo.com", "bar.com"] }); + + // We need to keep a private-browsing window active, otherwise the + // 'last-pb-context-exited' notification will be dispatched. + const privateBrowsingHolder = await CookieXPCShellUtils.loadContentPage( + "http://bar.com/", + { privateBrowsing: true } + ); + + // Create URIs pointing to foo.com and bar.com. + let uri1 = NetUtil.newURI("http://foo.com/foo.html"); + let uri2 = NetUtil.newURI("http://bar.com/bar.html"); + + // Set a cookie for host 1. + Services.cookies.setCookieStringFromHttp( + uri1, + "oh=hai; max-age=1000", + make_channel(uri1.spec) + ); + Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 1); + + // Enter private browsing mode, set a cookie for host 2, and check the counts. + var chan1 = make_channel(uri1.spec); + chan1.QueryInterface(Ci.nsIPrivateBrowsingChannel); + chan1.setPrivate(true); + + var chan2 = make_channel(uri2.spec); + chan2.QueryInterface(Ci.nsIPrivateBrowsingChannel); + chan2.setPrivate(true); + + Services.cookies.setCookieStringFromHttp(uri2, "oh=hai; max-age=1000", chan2); + Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), ""); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "oh=hai"); + + // Remove cookies and check counts. + Services.obs.notifyObservers(null, "last-pb-context-exited"); + Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), ""); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), ""); + + Services.cookies.setCookieStringFromHttp(uri2, "oh=hai; max-age=1000", chan2); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "oh=hai"); + + // Leave private browsing mode and check counts. + Services.obs.notifyObservers(null, "last-pb-context-exited"); + Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 1); + Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0); + + // Fake a profile change. + await promise_close_profile(); + do_load_profile(); + + // Check that the right cookie persisted. + Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 1); + Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0); + + // Enter private browsing mode, set a cookie for host 2, and check the counts. + Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), ""); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), ""); + Services.cookies.setCookieStringFromHttp(uri2, "oh=hai; max-age=1000", chan2); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "oh=hai"); + + // Fake a profile change. + await promise_close_profile(); + do_load_profile(); + + // We're still in private browsing mode, but should have a new session. + // Check counts. + Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), ""); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), ""); + + // Leave private browsing mode and check counts. + Services.obs.notifyObservers(null, "last-pb-context-exited"); + Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 1); + Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0); + + // Enter private browsing mode. + + // Fake a profile change, but wait for async read completion. + await promise_close_profile(); + await promise_load_profile(); + + // We're still in private browsing mode, but should have a new session. + // Check counts. + Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), ""); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), ""); + + // Leave private browsing mode and check counts. + Services.obs.notifyObservers(null, "last-pb-context-exited"); + Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 1); + Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0); + + // Let's release the last PB window. + privateBrowsingHolder.close(); + Services.prefs.clearUserPref("dom.security.https_first"); +}); diff --git a/netwerk/test/unit/test_cookies_profile_close.js b/netwerk/test/unit/test_cookies_profile_close.js new file mode 100644 index 0000000000..6ea9ab23f3 --- /dev/null +++ b/netwerk/test/unit/test_cookies_profile_close.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the cookie APIs behave sanely after 'profile-before-change'. + +"use strict"; + +add_task(async () => { + // Set up a profile. + do_get_profile(); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + Services.prefs.setBoolPref("dom.security.https_first", false); + + // Start the cookieservice. + Services.cookies; + + CookieXPCShellUtils.createServer({ hosts: ["foo.com"] }); + + // Set a cookie. + let uri = NetUtil.newURI("http://foo.com"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + Services.scriptSecurityManager.createContentPrincipal(uri, {}); + + await CookieXPCShellUtils.setCookieToDocument( + uri.spec, + "oh=hai; max-age=1000" + ); + + let cookies = Services.cookies.cookies; + Assert.ok(cookies.length == 1); + let cookie = cookies[0]; + + // Fire 'profile-before-change'. + do_close_profile(); + + let promise = new _promise_observer("cookie-db-closed"); + + // Check that the APIs behave appropriately. + Assert.equal( + await CookieXPCShellUtils.getCookieStringFromDocument("http://foo.com/"), + "" + ); + + Assert.equal(Services.cookies.getCookieStringFromHttp(uri, channel), ""); + + await CookieXPCShellUtils.setCookieToDocument(uri.spec, "oh2=hai"); + + Services.cookies.setCookieStringFromHttp(uri, "oh3=hai", channel); + Assert.equal( + await CookieXPCShellUtils.getCookieStringFromDocument("http://foo.com/"), + "" + ); + + do_check_throws(function () { + Services.cookies.removeAll(); + }, Cr.NS_ERROR_NOT_AVAILABLE); + + do_check_throws(function () { + Services.cookies.cookies; + }, Cr.NS_ERROR_NOT_AVAILABLE); + + do_check_throws(function () { + Services.cookies.add( + "foo.com", + "", + "oh4", + "hai", + false, + false, + false, + 0, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + }, Cr.NS_ERROR_NOT_AVAILABLE); + + do_check_throws(function () { + Services.cookies.remove("foo.com", "", "oh4", {}); + }, Cr.NS_ERROR_NOT_AVAILABLE); + + do_check_throws(function () { + Services.cookies.cookieExists(cookie.host, cookie.path, cookie.name, {}); + }, Cr.NS_ERROR_NOT_AVAILABLE); + + do_check_throws(function () { + Services.cookies.countCookiesFromHost("foo.com"); + }, Cr.NS_ERROR_NOT_AVAILABLE); + + do_check_throws(function () { + Services.cookies.getCookiesFromHost("foo.com", {}); + }, Cr.NS_ERROR_NOT_AVAILABLE); + + // Wait for the database to finish closing. + await promise; + + // Load the profile and check that the API is available. + do_load_profile(); + Assert.ok( + Services.cookies.cookieExists(cookie.host, cookie.path, cookie.name, {}) + ); + Services.prefs.clearUserPref("dom.security.https_first"); +}); diff --git a/netwerk/test/unit/test_cookies_read.js b/netwerk/test/unit/test_cookies_read.js new file mode 100644 index 0000000000..d3cc329564 --- /dev/null +++ b/netwerk/test/unit/test_cookies_read.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// test cookie database asynchronous read operation. + +"use strict"; + +var CMAX = 1000; // # of cookies to create + +add_task(async () => { + // Set up a profile. + let profile = do_get_profile(); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // Bug 1617611 - Fix all the tests broken by "cookies SameSite=Lax by default" + Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false); + + // Start the cookieservice, to force creation of a database. + // Get the sessionCookies to join the initialization in cookie thread + Services.cookies.sessionCookies; + + // Open a database connection now, after synchronous initialization has + // completed. We may not be able to open one later once asynchronous writing + // begins. + Assert.ok(do_get_cookie_file(profile).exists()); + let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12); + + let uri = NetUtil.newURI("http://foo.com/"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + for (let i = 0; i < CMAX; ++i) { + let uri = NetUtil.newURI("http://" + i + ".com/"); + Services.cookies.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + } + + Assert.equal(do_count_cookies(), CMAX); + + // Wait until all CMAX cookies have been written out to the database. + while (do_count_cookies_in_db(db.db) < CMAX) { + await new Promise(resolve => executeSoon(resolve)); + } + + // Check the WAL file size. We set it to 16 pages of 32k, which means it + // should be around 500k. + let file = db.db.databaseFile; + Assert.ok(file.exists()); + Assert.ok(file.fileSize < 1e6); + db.close(); + + // fake a profile change + await promise_close_profile(); + do_load_profile(); + + // test a few random cookies + Assert.equal(Services.cookies.countCookiesFromHost("999.com"), 1); + Assert.equal(Services.cookies.countCookiesFromHost("abc.com"), 0); + Assert.equal(Services.cookies.countCookiesFromHost("100.com"), 1); + Assert.equal(Services.cookies.countCookiesFromHost("400.com"), 1); + Assert.equal(Services.cookies.countCookiesFromHost("xyz.com"), 0); + + // force synchronous load of everything + Assert.equal(do_count_cookies(), CMAX); + + // check that everything's precisely correct + for (let i = 0; i < CMAX; ++i) { + let host = i.toString() + ".com"; + Assert.equal(Services.cookies.countCookiesFromHost(host), 1); + } + + // reload again, to make sure the additions were written correctly + await promise_close_profile(); + do_load_profile(); + + // remove some of the cookies, in both reverse and forward order + for (let i = 100; i-- > 0; ) { + let host = i.toString() + ".com"; + Services.cookies.remove(host, "oh", "/", {}); + } + for (let i = CMAX - 100; i < CMAX; ++i) { + let host = i.toString() + ".com"; + Services.cookies.remove(host, "oh", "/", {}); + } + + // check the count + Assert.equal(do_count_cookies(), CMAX - 200); + + // reload again, to make sure the removals were written correctly + await promise_close_profile(); + do_load_profile(); + + // check the count + Assert.equal(do_count_cookies(), CMAX - 200); + + // reload again, but wait for async read completion + await promise_close_profile(); + await promise_load_profile(); + + // check that everything's precisely correct + Assert.equal(do_count_cookies(), CMAX - 200); + for (let i = 100; i < CMAX - 100; ++i) { + let host = i.toString() + ".com"; + Assert.equal(Services.cookies.countCookiesFromHost(host), 1); + } + + Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); diff --git a/netwerk/test/unit/test_cookies_sync_failure.js b/netwerk/test/unit/test_cookies_sync_failure.js new file mode 100644 index 0000000000..d1c2b9fa78 --- /dev/null +++ b/netwerk/test/unit/test_cookies_sync_failure.js @@ -0,0 +1,344 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the various ways opening a cookie database can fail in a synchronous +// (i.e. immediate) manner, and that the database is renamed and recreated +// under each circumstance. These circumstances are, in no particular order: +// +// 1) A corrupt database, such that opening the connection fails. +// 2) The 'moz_cookies' table doesn't exist. +// 3) Not all of the expected columns exist, and statement creation fails when: +// a) The schema version is larger than the current version. +// b) The schema version is less than or equal to the current version. +// 4) Migration fails. This will have different modes depending on the initial +// version: +// a) Schema 1: the 'lastAccessed' column already exists. +// b) Schema 2: the 'baseDomain' column already exists; or 'baseDomain' +// cannot be computed for a particular host. +// c) Schema 3: the 'creationTime' column already exists; or the +// 'moz_uniqueid' index already exists. + +"use strict"; + +let profile; +let cookieFile; +let backupFile; +let sub_generator; +let now; +let futureExpiry; +let cookie; + +var COOKIE_DATABASE_SCHEMA_CURRENT = 12; + +var test_generator = do_run_test(); + +function run_test() { + do_test_pending(); + do_run_generator(test_generator); +} + +function finish_test() { + executeSoon(function () { + test_generator.return(); + do_test_finished(); + }); +} + +function* do_run_test() { + // Set up a profile. + profile = do_get_profile(); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // Get the cookie file and the backup file. + cookieFile = profile.clone(); + cookieFile.append("cookies.sqlite"); + backupFile = profile.clone(); + backupFile.append("cookies.sqlite.bak"); + Assert.ok(!cookieFile.exists()); + Assert.ok(!backupFile.exists()); + + // Create a cookie object for testing. + now = Date.now() * 1000; + futureExpiry = Math.round(now / 1e6 + 1000); + cookie = new Cookie( + "oh", + "hai", + "bar.com", + "/", + futureExpiry, + now, + now, + false, + false, + false + ); + + sub_generator = run_test_1(test_generator); + sub_generator.next(); + yield; + + sub_generator = run_test_2(test_generator); + sub_generator.next(); + yield; + + sub_generator = run_test_3(test_generator, 99); + sub_generator.next(); + yield; + + sub_generator = run_test_3(test_generator, COOKIE_DATABASE_SCHEMA_CURRENT); + sub_generator.next(); + yield; + + sub_generator = run_test_3(test_generator, 4); + sub_generator.next(); + yield; + + sub_generator = run_test_3(test_generator, 3); + sub_generator.next(); + yield; + + sub_generator = run_test_4_exists( + test_generator, + 1, + "ALTER TABLE moz_cookies ADD lastAccessed INTEGER" + ); + sub_generator.next(); + yield; + + sub_generator = run_test_4_exists( + test_generator, + 2, + "ALTER TABLE moz_cookies ADD baseDomain TEXT" + ); + sub_generator.next(); + yield; + + sub_generator = run_test_4_baseDomain(test_generator); + sub_generator.next(); + yield; + + sub_generator = run_test_4_exists( + test_generator, + 3, + "ALTER TABLE moz_cookies ADD creationTime INTEGER" + ); + sub_generator.next(); + yield; + + sub_generator = run_test_4_exists( + test_generator, + 3, + "CREATE UNIQUE INDEX moz_uniqueid ON moz_cookies (name, host, path)" + ); + sub_generator.next(); + yield; + + finish_test(); +} + +const garbage = "hello thar!"; + +function create_garbage_file(file) { + // Create an empty database file. + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, -1); + Assert.ok(file.exists()); + Assert.equal(file.fileSize, 0); + + // Write some garbage to it. + let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init(file, -1, -1, 0); + ostream.write(garbage, garbage.length); + ostream.flush(); + ostream.close(); + + file = file.clone(); // Windows maintains a stat cache. It's lame. + Assert.equal(file.fileSize, garbage.length); +} + +function check_garbage_file(file) { + Assert.ok(file.exists()); + Assert.equal(file.fileSize, garbage.length); + file.remove(false); + Assert.ok(!file.exists()); +} + +function* run_test_1(generator) { + // Create a garbage database file. + create_garbage_file(cookieFile); + + let uri = NetUtil.newURI("http://foo.com/"); + const channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + // Load the profile and populate it. + Services.cookies.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + + // Fake a profile change. + do_close_profile(sub_generator); + yield; + do_load_profile(); + + // Check that the new database contains the cookie, and the old file was + // renamed. + Assert.equal(do_count_cookies(), 1); + check_garbage_file(backupFile); + + // Close the profile. + do_close_profile(sub_generator); + yield; + + // Clean up. + cookieFile.remove(false); + Assert.ok(!cookieFile.exists()); + do_run_generator(generator); +} + +function* run_test_2(generator) { + // Load the profile and populate it. + do_load_profile(); + let uri = NetUtil.newURI("http://foo.com/"); + const channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + Services.cookies.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + + // Fake a profile change. + do_close_profile(sub_generator); + yield; + + // Drop the table. + let db = Services.storage.openDatabase(cookieFile); + db.executeSimpleSQL("DROP TABLE moz_cookies"); + db.close(); + + // Load the profile and check that the table is recreated in-place. + do_load_profile(); + Assert.equal(do_count_cookies(), 0); + Assert.ok(!backupFile.exists()); + + // Close the profile. + do_close_profile(sub_generator); + yield; + + // Clean up. + cookieFile.remove(false); + Assert.ok(!cookieFile.exists()); + do_run_generator(generator); +} + +function* run_test_3(generator, schema) { + // Manually create a schema 2 database, populate it, and set the schema + // version to the desired number. + let schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2); + schema2db.insertCookie(cookie); + schema2db.db.schemaVersion = schema; + schema2db.close(); + + // Load the profile and check that the column existence test fails. + do_load_profile(); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + do_close_profile(sub_generator); + yield; + + // Check that the schema version has been reset. + let db = Services.storage.openDatabase(cookieFile); + Assert.equal(db.schemaVersion, COOKIE_DATABASE_SCHEMA_CURRENT); + db.close(); + + // Clean up. + cookieFile.remove(false); + Assert.ok(!cookieFile.exists()); + do_run_generator(generator); +} + +function* run_test_4_exists(generator, schema, stmt) { + // Manually create a database, populate it, and add the desired column. + let db = new CookieDatabaseConnection(do_get_cookie_file(profile), schema); + db.insertCookie(cookie); + db.db.executeSimpleSQL(stmt); + db.close(); + + // Load the profile and check that migration fails. + do_load_profile(); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + do_close_profile(sub_generator); + yield; + + // Check that the schema version has been reset and the backup file exists. + db = Services.storage.openDatabase(cookieFile); + Assert.equal(db.schemaVersion, COOKIE_DATABASE_SCHEMA_CURRENT); + db.close(); + Assert.ok(backupFile.exists()); + + // Clean up. + cookieFile.remove(false); + backupFile.remove(false); + Assert.ok(!cookieFile.exists()); + Assert.ok(!backupFile.exists()); + do_run_generator(generator); +} + +function* run_test_4_baseDomain(generator) { + // Manually create a database and populate it with a bad host. + let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2); + let badCookie = new Cookie( + "oh", + "hai", + ".", + "/", + futureExpiry, + now, + now, + false, + false, + false + ); + db.insertCookie(badCookie); + db.close(); + + // Load the profile and check that migration fails. + do_load_profile(); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + do_close_profile(sub_generator); + yield; + + // Check that the schema version has been reset and the backup file exists. + db = Services.storage.openDatabase(cookieFile); + Assert.equal(db.schemaVersion, COOKIE_DATABASE_SCHEMA_CURRENT); + db.close(); + Assert.ok(backupFile.exists()); + + // Clean up. + cookieFile.remove(false); + backupFile.remove(false); + Assert.ok(!cookieFile.exists()); + Assert.ok(!backupFile.exists()); + do_run_generator(generator); +} diff --git a/netwerk/test/unit/test_cookies_thirdparty.js b/netwerk/test/unit/test_cookies_thirdparty.js new file mode 100644 index 0000000000..5d34e3f999 --- /dev/null +++ b/netwerk/test/unit/test_cookies_thirdparty.js @@ -0,0 +1,168 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// test third party cookie blocking, for the cases: +// 1) with null channel +// 2) with channel, but with no docshell parent + +"use strict"; + +add_task(async () => { + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + Services.prefs.setBoolPref( + "network.cookie.rejectForeignWithExceptions.enabled", + false + ); + Services.prefs.setBoolPref("dom.security.https_first", false); + + // Bug 1617611 - Fix all the tests broken by "cookies SameSite=Lax by default" + Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false); + + CookieXPCShellUtils.createServer({ + hosts: ["foo.com", "bar.com", "third.com"], + }); + + function createChannel(uri, principal = null) { + const channel = NetUtil.newChannel({ + uri, + loadingPrincipal: + principal || + Services.scriptSecurityManager.createContentPrincipal(uri, {}), + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + return channel.QueryInterface(Ci.nsIHttpChannelInternal); + } + + // Create URIs and channels pointing to foo.com and bar.com. + // We will use these to put foo.com into first and third party contexts. + let spec1 = "http://foo.com/foo.html"; + let spec2 = "http://bar.com/bar.html"; + let uri1 = NetUtil.newURI(spec1); + let uri2 = NetUtil.newURI(spec2); + + // test with cookies enabled + { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_ACCEPT + ); + + let channel1 = createChannel(uri1); + let channel2 = createChannel(uri2); + + await do_set_cookies(uri1, channel1, true, [1, 2]); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, true, [1, 2]); + Services.cookies.removeAll(); + } + + // test with third party cookies blocked + { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN + ); + + let channel1 = createChannel(uri1); + let channel2 = createChannel(uri2); + + await do_set_cookies(uri1, channel1, true, [0, 1]); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, true, [0, 0]); + Services.cookies.removeAll(); + } + + // test with third party cookies blocked using system principal + { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN + ); + + let channel1 = createChannel( + uri1, + Services.scriptSecurityManager.getSystemPrincipal() + ); + let channel2 = createChannel( + uri2, + Services.scriptSecurityManager.getSystemPrincipal() + ); + + await do_set_cookies(uri1, channel1, true, [0, 1]); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, true, [0, 0]); + Services.cookies.removeAll(); + } + + // Force the channel URI to be used when determining the originating URI of + // the channel. + // test with third party cookies blocked + + // test with cookies enabled + { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_ACCEPT + ); + + let channel1 = createChannel(uri1); + channel1.forceAllowThirdPartyCookie = true; + + let channel2 = createChannel(uri2); + channel2.forceAllowThirdPartyCookie = true; + + await do_set_cookies(uri1, channel1, true, [1, 2]); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, true, [1, 2]); + Services.cookies.removeAll(); + } + + // test with third party cookies blocked + { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN + ); + + let channel1 = createChannel(uri1); + channel1.forceAllowThirdPartyCookie = true; + + let channel2 = createChannel(uri2); + channel2.forceAllowThirdPartyCookie = true; + + await do_set_cookies(uri1, channel1, true, [0, 1]); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, true, [0, 0]); + Services.cookies.removeAll(); + } + + // test with third party cookies limited + { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN + ); + + let channel1 = createChannel(uri1); + channel1.forceAllowThirdPartyCookie = true; + + let channel2 = createChannel(uri2); + channel2.forceAllowThirdPartyCookie = true; + + await do_set_cookies(uri1, channel1, true, [0, 1]); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, true, [0, 0]); + Services.cookies.removeAll(); + do_set_single_http_cookie(uri1, channel1, 1); + await do_set_cookies(uri1, channel2, true, [1, 2]); + Services.cookies.removeAll(); + } + Services.prefs.clearUserPref("dom.security.https_first"); + Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); diff --git a/netwerk/test/unit/test_cookies_thirdparty_session.js b/netwerk/test/unit/test_cookies_thirdparty_session.js new file mode 100644 index 0000000000..eefd5d87f9 --- /dev/null +++ b/netwerk/test/unit/test_cookies_thirdparty_session.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// test third party persistence across sessions, for the cases: +// 1) network.cookie.thirdparty.sessionOnly = false +// 2) network.cookie.thirdparty.sessionOnly = true + +"use strict"; + +add_task(async () => { + // Set up a profile. + do_get_profile(); + + // We don't want to have CookieJarSettings blocking this test. + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + Services.prefs.setBoolPref("dom.security.https_first", false); + + // Bug 1617611 - Fix all the tests broken by "cookies SameSite=Lax by default" + Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false); + + CookieXPCShellUtils.createServer({ + hosts: ["foo.com", "bar.com", "third.com"], + }); + + // Create URIs and channels pointing to foo.com and bar.com. + // We will use these to put foo.com into first and third party contexts. + var spec1 = "http://foo.com/foo.html"; + var spec2 = "http://bar.com/bar.html"; + var uri1 = NetUtil.newURI(spec1); + var uri2 = NetUtil.newURI(spec2); + var channel1 = NetUtil.newChannel({ + uri: uri1, + loadUsingSystemPrincipal: true, + }); + var channel2 = NetUtil.newChannel({ + uri: uri2, + loadUsingSystemPrincipal: true, + }); + + // Force the channel URI to be used when determining the originating URI of + // the channel. + var httpchannel1 = channel1.QueryInterface(Ci.nsIHttpChannelInternal); + var httpchannel2 = channel2.QueryInterface(Ci.nsIHttpChannelInternal); + httpchannel1.forceAllowThirdPartyCookie = true; + httpchannel2.forceAllowThirdPartyCookie = true; + + // test with cookies enabled, and third party cookies persistent. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref("network.cookie.thirdparty.sessionOnly", false); + await do_set_cookies(uri1, channel2, false, [1, 2]); + await do_set_cookies(uri2, channel1, true, [1, 2]); + + // fake a profile change + await promise_close_profile(); + + do_load_profile(); + Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 2); + Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0); + + // test with third party cookies for session only. + Services.prefs.setBoolPref("network.cookie.thirdparty.sessionOnly", true); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, false, [1, 2]); + await do_set_cookies(uri2, channel1, true, [1, 2]); + + // fake a profile change + await promise_close_profile(); + + do_load_profile(); + Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 0); + Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0); + Services.prefs.clearUserPref("dom.security.https_first"); + Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); diff --git a/netwerk/test/unit/test_cookies_upgrade_10.js b/netwerk/test/unit/test_cookies_upgrade_10.js new file mode 100644 index 0000000000..c845a096d5 --- /dev/null +++ b/netwerk/test/unit/test_cookies_upgrade_10.js @@ -0,0 +1,61 @@ +/* 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 getDBVersion(dbfile) { + let dbConnection = Services.storage.openDatabase(dbfile); + let version = dbConnection.schemaVersion; + dbConnection.close(); + + return version; +} + +function indexExists(dbfile, indexname) { + let dbConnection = Services.storage.openDatabase(dbfile); + let result = dbConnection.indexExists(indexname); + dbConnection.close(); + + return result; +} + +add_task(async function () { + try { + let testfile = do_get_file("data/cookies_v10.sqlite"); + let profileDir = do_get_profile(); + + // Cleanup from any previous tests or failures. + let destFile = profileDir.clone(); + destFile.append("cookies.sqlite"); + if (destFile.exists()) { + destFile.remove(false); + } + + testfile.copyTo(profileDir, "cookies.sqlite"); + Assert.equal(10, getDBVersion(destFile)); + + Assert.ok(destFile.exists()); + + // Check that the index exists + Assert.ok(indexExists(destFile, "moz_basedomain")); + + // Do something that will cause the cookie service access and upgrade the + // database. + Services.cookies.cookies; + + // Pretend that we're about to shut down, to tell the cookie manager + // to clean up its connection with its database. + Services.startup.advanceShutdownPhase( + Services.startup.SHUTDOWN_PHASE_APPSHUTDOWN + ); + + // check for upgraded schema. + Assert.equal(12, getDBVersion(destFile)); + + // Check that the index was deleted + Assert.ok(!indexExists(destFile, "moz_basedomain")); + } catch (e) { + throw new Error(`FAILED: ${e}`); + } +}); diff --git a/netwerk/test/unit/test_data_protocol.js b/netwerk/test/unit/test_data_protocol.js new file mode 100644 index 0000000000..3ef19cd8ea --- /dev/null +++ b/netwerk/test/unit/test_data_protocol.js @@ -0,0 +1,91 @@ +/* run some tests on the data: protocol handler */ + +// The behaviour wrt spaces is: +// - Textual content keeps all spaces +// - Other content strips unescaped spaces +// - Base64 content strips escaped and unescaped spaces + +"use strict"; + +var urls = [ + ["data:,", "text/plain", ""], + ["data:,foo", "text/plain", "foo"], + [ + "data:application/octet-stream,foo bar", + "application/octet-stream", + "foo bar", + ], + [ + "data:application/octet-stream,foo%20bar", + "application/octet-stream", + "foo bar", + ], + ["data:application/xhtml+xml,foo bar", "application/xhtml+xml", "foo bar"], + ["data:application/xhtml+xml,foo%20bar", "application/xhtml+xml", "foo bar"], + ["data:text/plain,foo%00 bar", "text/plain", "foo\x00 bar"], + ["data:text/plain;x=y,foo%00 bar", "text/plain", "foo\x00 bar"], + ["data:;x=y,foo%00 bar", "text/plain", "foo\x00 bar"], + ["data:text/plain;base64,Zm9 vI%20GJ%0Dhc%0Ag==", "text/plain", "foo bar"], + ["DATA:TEXT/PLAIN;BASE64,Zm9 vI%20GJ%0Dhc%0Ag==", "text/plain", "foo bar"], + ["DaTa:;BaSe64,Zm9 vI%20GJ%0Dhc%0Ag==", "text/plain", "foo bar"], + ["data:;x=y;base64,Zm9 vI%20GJ%0Dhc%0Ag==", "text/plain", "foo bar"], + // Bug 774240 + [ + "data:application/octet-stream;base64=y,foobar", + "application/octet-stream", + "foobar", + ], + // Bug 781693 + ["data:text/plain;base64;x=y,dGVzdA==", "text/plain", "test"], + ["data:text/plain;x=y;base64,dGVzdA==", "text/plain", "test"], + ["data:text/plain;x=y;base64,", "text/plain", ""], + ["data: ;charset=x ; base64,WA", "text/plain", "X", "x"], + ["data:base64,WA", "text/plain", "WA", "US-ASCII"], +]; + +function run_test() { + dump("*** run_test\n"); + + function on_read_complete(request, data, idx) { + dump("*** run_test.on_read_complete\n"); + + if (request.nsIChannel.contentType != urls[idx][1]) { + do_throw( + "Type mismatch! Is <" + + chan.contentType + + ">, should be <" + + urls[idx][1] + + ">" + ); + } + + if (urls[idx][3] && request.nsIChannel.contentCharset !== urls[idx][3]) { + do_throw( + `Charset mismatch! Test <${urls[idx][0]}> - Is <${request.nsIChannel.contentCharset}>, should be <${urls[idx][3]}>` + ); + } + + /* read completed successfully. now compare the data. */ + if (data != urls[idx][2]) { + do_throw( + "Stream contents do not match with direct read! Is <" + + data + + ">, should be <" + + urls[idx][2] + + ">" + ); + } + do_test_finished(); + } + + for (var i = 0; i < urls.length; ++i) { + dump("*** opening channel " + i + "\n"); + do_test_pending(); + var chan = NetUtil.newChannel({ + uri: urls[i][0], + loadUsingSystemPrincipal: true, + }); + chan.contentType = "foo/bar"; // should be ignored + chan.asyncOpen(new ChannelListener(on_read_complete, i)); + } +} diff --git a/netwerk/test/unit/test_defaultURI.js b/netwerk/test/unit/test_defaultURI.js new file mode 100644 index 0000000000..92ac3042b3 --- /dev/null +++ b/netwerk/test/unit/test_defaultURI.js @@ -0,0 +1,185 @@ +"use strict"; + +function stringToDefaultURI(str) { + return Cc["@mozilla.org/network/default-uri-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec(str) + .finalize(); +} + +add_task(function test_getters() { + let uri = stringToDefaultURI( + "proto://user:password@hostname:123/path/to/file?query#hash" + ); + equal(uri.spec, "proto://user:password@hostname:123/path/to/file?query#hash"); + equal(uri.prePath, "proto://user:password@hostname:123"); + equal(uri.scheme, "proto"); + equal(uri.userPass, "user:password"); + equal(uri.username, "user"); + equal(uri.password, "password"); + equal(uri.hostPort, "hostname:123"); + equal(uri.host, "hostname"); + equal(uri.port, 123); + equal(uri.pathQueryRef, "/path/to/file?query#hash"); + equal(uri.asciiSpec, uri.spec); + equal(uri.asciiHostPort, uri.hostPort); + equal(uri.asciiHost, uri.host); + equal(uri.ref, "hash"); + equal( + uri.specIgnoringRef, + "proto://user:password@hostname:123/path/to/file?query" + ); + equal(uri.hasRef, true); + equal(uri.filePath, "/path/to/file"); + equal(uri.query, "query"); + equal(uri.displayHost, uri.host); + equal(uri.displayHostPort, uri.hostPort); + equal(uri.displaySpec, uri.spec); + equal(uri.displayPrePath, uri.prePath); +}); + +add_task(function test_methods() { + let uri = stringToDefaultURI( + "proto://user:password@hostname:123/path/to/file?query#hash" + ); + let uri_same = stringToDefaultURI( + "proto://user:password@hostname:123/path/to/file?query#hash" + ); + let uri_different = stringToDefaultURI( + "proto://user:password@hostname:123/path/to/file?query" + ); + let uri_very_different = stringToDefaultURI( + "proto://user:password@hostname:123/path/to/file?query1#hash" + ); + ok(uri.equals(uri_same)); + ok(!uri.equals(uri_different)); + ok(uri.schemeIs("proto")); + ok(!uri.schemeIs("proto2")); + ok(!uri.schemeIs("proto ")); + ok(!uri.schemeIs("proto\n")); + equal(uri.resolve("/hello"), "proto://user:password@hostname:123/hello"); + equal( + uri.resolve("hello"), + "proto://user:password@hostname:123/path/to/hello" + ); + equal(uri.resolve("proto2:otherhost"), "proto2:otherhost"); + ok(uri.equalsExceptRef(uri_same)); + ok(uri.equalsExceptRef(uri_different)); + ok(!uri.equalsExceptRef(uri_very_different)); +}); + +add_task(function test_mutator() { + let uri = stringToDefaultURI( + "proto://user:pass@host:123/path/to/file?query#hash" + ); + + let check = (callSetters, verify) => { + let m = uri.mutate(); + callSetters(m); + verify(m.finalize()); + }; + + check( + m => m.setSpec("test:bla"), + u => equal(u.spec, "test:bla") + ); + check( + m => m.setSpec("test:bla"), + u => equal(u.spec, "test:bla") + ); + check( + m => m.setScheme("some"), + u => equal(u.spec, "some://user:pass@host:123/path/to/file?query#hash") + ); + check( + m => m.setUserPass("u"), + u => equal(u.spec, "proto://u@host:123/path/to/file?query#hash") + ); + check( + m => m.setUserPass("u:p"), + u => equal(u.spec, "proto://u:p@host:123/path/to/file?query#hash") + ); + check( + m => m.setUserPass(":p"), + u => equal(u.spec, "proto://:p@host:123/path/to/file?query#hash") + ); + check( + m => m.setUserPass(""), + u => equal(u.spec, "proto://host:123/path/to/file?query#hash") + ); + check( + m => m.setUsername("u"), + u => equal(u.spec, "proto://u:pass@host:123/path/to/file?query#hash") + ); + check( + m => m.setPassword("p"), + u => equal(u.spec, "proto://user:p@host:123/path/to/file?query#hash") + ); + check( + m => m.setHostPort("h"), + u => equal(u.spec, "proto://user:pass@h:123/path/to/file?query#hash") + ); + check( + m => m.setHostPort("h:456"), + u => equal(u.spec, "proto://user:pass@h:456/path/to/file?query#hash") + ); + check( + m => m.setHost("bla"), + u => equal(u.spec, "proto://user:pass@bla:123/path/to/file?query#hash") + ); + check( + m => m.setPort(987), + u => equal(u.spec, "proto://user:pass@host:987/path/to/file?query#hash") + ); + check( + m => m.setPathQueryRef("/p?q#r"), + u => equal(u.spec, "proto://user:pass@host:123/p?q#r") + ); + check( + m => m.setRef("r"), + u => equal(u.spec, "proto://user:pass@host:123/path/to/file?query#r") + ); + check( + m => m.setFilePath("/my/path"), + u => equal(u.spec, "proto://user:pass@host:123/my/path?query#hash") + ); + check( + m => m.setQuery("q"), + u => equal(u.spec, "proto://user:pass@host:123/path/to/file?q#hash") + ); +}); + +add_task(function test_ipv6() { + let uri = stringToDefaultURI("non-special://[2001::1]/"); + equal(uri.hostPort, "[2001::1]"); + // Hopefully this will change after bug 1603199. + equal(uri.host, "2001::1"); +}); + +add_task(function test_serialization() { + let uri = stringToDefaultURI("http://example.org/path"); + let str = serialize_to_escaped_string(uri); + let other = deserialize_from_escaped_string(str).QueryInterface(Ci.nsIURI); + equal(other.spec, uri.spec); +}); + +// This test assumes the serialization never changes, which might not be true. +// It's OK to change the test if we ever make changes to the serialization +// code and this starts failing. +add_task(function test_deserialize_from_string() { + let payload = + "%04DZ%A0%FD%27L%99%BDAk%E61%8A%E9%2C%00%00%00%00%00%00%00" + + "%00%C0%00%00%00%00%00%00F%00%00%00%13scheme%3Astuff/to/say"; + equal( + deserialize_from_escaped_string(payload).QueryInterface(Ci.nsIURI).spec, + stringToDefaultURI("scheme:stuff/to/say").spec + ); + + let payload2 = + "%04DZ%A0%FD%27L%99%BDAk%E61%8A%E9%2C%00%00%00%00%00%00%00" + + "%00%C0%00%00%00%00%00%00F%00%00%00%17http%3A//example.org/path"; + equal( + deserialize_from_escaped_string(payload2).QueryInterface(Ci.nsIURI).spec, + stringToDefaultURI("http://example.org/path").spec + ); +}); diff --git a/netwerk/test/unit/test_dns_by_type_resolve.js b/netwerk/test/unit/test_dns_by_type_resolve.js new file mode 100644 index 0000000000..26b087f301 --- /dev/null +++ b/netwerk/test/unit/test_dns_by_type_resolve.js @@ -0,0 +1,83 @@ +/* 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"; + +let h2Port; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +add_setup(async function setup() { + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + trr_test_setup(); + registerCleanupFunction(() => { + trr_clear_prefs(); + }); + + if (mozinfo.socketprocess_networking) { + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } + + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); +}); + +let test_answer = "bXkgdm9pY2UgaXMgbXkgcGFzc3dvcmQ="; +let test_answer_addr = "127.0.0.1"; + +add_task(async function testTXTResolve() { + // use the h2 server as DOH provider + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/doh" + ); + + let { inRecord } = await new TRRDNSListener("_esni.example.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_TXT, + }); + + let answer = inRecord + .QueryInterface(Ci.nsIDNSTXTRecord) + .getRecordsAsOneString(); + Assert.equal(answer, test_answer, "got correct answer"); +}); + +// verify TXT record pushed on a A record request +add_task(async function testTXTRecordPushPart1() { + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/txt-dns-push" + ); + let { inRecord } = await new TRRDNSListener("_esni_push.example.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + expectedAnswer: "127.0.0.1", + }); + + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + let answer = inRecord.getNextAddrAsString(); + Assert.equal(answer, test_answer_addr, "got correct answer"); +}); + +// verify the TXT pushed record +add_task(async function testTXTRecordPushPart2() { + // At this point the second host name should've been pushed and we can resolve it using + // cache only. Set back the URI to a path that fails. + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/404" + ); + let { inRecord } = await new TRRDNSListener("_esni_push.example.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_TXT, + }); + + let answer = inRecord + .QueryInterface(Ci.nsIDNSTXTRecord) + .getRecordsAsOneString(); + Assert.equal(answer, test_answer, "got correct answer"); +}); diff --git a/netwerk/test/unit/test_dns_cancel.js b/netwerk/test/unit/test_dns_cancel.js new file mode 100644 index 0000000000..c20e63ae4c --- /dev/null +++ b/netwerk/test/unit/test_dns_cancel.js @@ -0,0 +1,119 @@ +"use strict"; + +var hostname1 = ""; +var hostname2 = ""; +var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +for (var i = 0; i < 20; i++) { + hostname1 += possible.charAt(Math.floor(Math.random() * possible.length)); + hostname2 += possible.charAt(Math.floor(Math.random() * possible.length)); +} + +var requestList1Canceled2; +var requestList1NotCanceled; + +var requestList2Canceled; +var requestList2NotCanceled; + +var listener1 = { + onLookupComplete(inRequest, inRecord, inStatus) { + // One request should be resolved and two request should be canceled. + if (inRequest == requestList1NotCanceled) { + // This request should not be canceled. + Assert.notEqual(inStatus, Cr.NS_ERROR_ABORT); + + do_test_finished(); + } + }, + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), +}; + +var listener2 = { + onLookupComplete(inRequest, inRecord, inStatus) { + // One request should be resolved and the other canceled. + if (inRequest == requestList2NotCanceled) { + // The request should not be canceled. + Assert.notEqual(inStatus, Cr.NS_ERROR_ABORT); + + do_test_finished(); + } + }, + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), +}; + +const defaultOriginAttributes = {}; + +function run_test() { + var mainThread = Services.tm.currentThread; + + var flags = Ci.nsIDNSService.RESOLVE_BYPASS_CACHE; + + // This one will be canceled with cancelAsyncResolve. + Services.dns.asyncResolve( + hostname2, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener1, + mainThread, + defaultOriginAttributes + ); + Services.dns.cancelAsyncResolve( + hostname2, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener1, + Cr.NS_ERROR_ABORT, + defaultOriginAttributes + ); + + // This one will not be canceled. + requestList1NotCanceled = Services.dns.asyncResolve( + hostname1, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener1, + mainThread, + defaultOriginAttributes + ); + + // This one will be canceled with cancel(Cr.NS_ERROR_ABORT). + requestList1Canceled2 = Services.dns.asyncResolve( + hostname1, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener1, + mainThread, + defaultOriginAttributes + ); + requestList1Canceled2.cancel(Cr.NS_ERROR_ABORT); + + // This one will not be canceled. + requestList2NotCanceled = Services.dns.asyncResolve( + hostname1, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener2, + mainThread, + defaultOriginAttributes + ); + + // This one will be canceled with cancel(Cr.NS_ERROR_ABORT). + requestList2Canceled = Services.dns.asyncResolve( + hostname2, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener2, + mainThread, + defaultOriginAttributes + ); + requestList2Canceled.cancel(Cr.NS_ERROR_ABORT); + + do_test_pending(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_dns_disable_ipv4.js b/netwerk/test/unit/test_dns_disable_ipv4.js new file mode 100644 index 0000000000..48a97f2399 --- /dev/null +++ b/netwerk/test/unit/test_dns_disable_ipv4.js @@ -0,0 +1,68 @@ +// +// Tests that calling asyncResolve with the RESOLVE_DISABLE_IPV4 flag doesn't +// return any IPv4 addresses. +// + +"use strict"; + +const gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); + +const defaultOriginAttributes = {}; + +add_task(async function test_none() { + let [, inRecord] = await new Promise(resolve => { + let listener = { + onLookupComplete(inRequest, inRecord, inStatus) { + resolve([inRequest, inRecord, inStatus]); + }, + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), + }; + + Services.dns.asyncResolve( + "example.org", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_DISABLE_IPV4, + null, // resolverInfo + listener, + null, + defaultOriginAttributes + ); + }); + + if (inRecord && inRecord.QueryInterface(Ci.nsIDNSAddrRecord)) { + while (inRecord.hasMore()) { + let nextIP = inRecord.getNextAddrAsString(); + ok(nextIP.includes(":"), `${nextIP} should be IPv6`); + } + } +}); + +add_task(async function test_some() { + Services.dns.clearCache(true); + gOverride.addIPOverride("example.com", "1.1.1.1"); + gOverride.addIPOverride("example.org", "::1:2:3"); + let [, inRecord] = await new Promise(resolve => { + let listener = { + onLookupComplete(inRequest, inRecord, inStatus) { + resolve([inRequest, inRecord, inStatus]); + }, + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), + }; + + Services.dns.asyncResolve( + "example.org", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_DISABLE_IPV4, + null, // resolverInfo + listener, + null, + defaultOriginAttributes + ); + }); + + ok(inRecord.QueryInterface(Ci.nsIDNSAddrRecord)); + equal(inRecord.getNextAddrAsString(), "::1:2:3"); + equal(inRecord.hasMore(), false); +}); diff --git a/netwerk/test/unit/test_dns_disable_ipv6.js b/netwerk/test/unit/test_dns_disable_ipv6.js new file mode 100644 index 0000000000..d05c56091f --- /dev/null +++ b/netwerk/test/unit/test_dns_disable_ipv6.js @@ -0,0 +1,51 @@ +// +// Tests that calling asyncResolve with the RESOLVE_DISABLE_IPV6 flag doesn't +// return any IPv6 addresses. +// + +"use strict"; + +var listener = { + onLookupComplete(inRequest, inRecord, inStatus) { + if (inStatus != Cr.NS_OK) { + Assert.equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); + do_test_finished(); + return; + } + + while (true) { + try { + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + // If there is an answer it should be an IPv4 address + dump(answer); + Assert.ok(!answer.includes(":")); + Assert.ok(answer.includes(".")); + } catch (e) { + break; + } + } + do_test_finished(); + }, +}; + +const defaultOriginAttributes = {}; + +function run_test() { + do_test_pending(); + try { + Services.dns.asyncResolve( + "example.com", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_DISABLE_IPV6, + null, // resolverInfo + listener, + null, + defaultOriginAttributes + ); + } catch (e) { + dump(e); + Assert.ok(false); + do_test_finished(); + } +} diff --git a/netwerk/test/unit/test_dns_disabled.js b/netwerk/test/unit/test_dns_disabled.js new file mode 100644 index 0000000000..cfffd5530f --- /dev/null +++ b/netwerk/test/unit/test_dns_disabled.js @@ -0,0 +1,88 @@ +"use strict"; + +const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); +const mainThread = Services.tm.currentThread; + +function makeListenerBlock(next) { + return { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.ok(!Components.isSuccessCode(inStatus)); + next(); + }, + }; +} + +function makeListenerDontBlock(next, expectedAnswer) { + return { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_OK); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + if (expectedAnswer) { + Assert.equal(answer, expectedAnswer); + } else { + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + } + next(); + }, + }; +} + +function do_test({ dnsDisabled, mustBlock, testDomain, expectedAnswer }) { + return new Promise(resolve => { + Services.prefs.setBoolPref("network.dns.disabled", dnsDisabled); + try { + Services.dns.asyncResolve( + testDomain, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + mustBlock + ? makeListenerBlock(resolve) + : makeListenerDontBlock(resolve, expectedAnswer), + mainThread, + {} // Default origin attributes + ); + } catch (e) { + Assert.ok(mustBlock === true); + resolve(); + } + }); +} + +function setup() { + override.addIPOverride("foo.bar", "127.0.0.1"); + registerCleanupFunction(function () { + override.clearOverrides(); + Services.prefs.clearUserPref("network.dns.disabled"); + }); +} +setup(); + +// IP literals should be resolved even if dns is disabled +add_task(async function testIPLiteral() { + return do_test({ + dnsDisabled: true, + mustBlock: false, + testDomain: "0x01010101", + expectedAnswer: "1.1.1.1", + }); +}); + +add_task(async function testBlocked() { + return do_test({ + dnsDisabled: true, + mustBlock: true, + testDomain: "foo.bar", + }); +}); + +add_task(async function testNotBlocked() { + return do_test({ + dnsDisabled: false, + mustBlock: false, + testDomain: "foo.bar", + }); +}); diff --git a/netwerk/test/unit/test_dns_localredirect.js b/netwerk/test/unit/test_dns_localredirect.js new file mode 100644 index 0000000000..3ca432f477 --- /dev/null +++ b/netwerk/test/unit/test_dns_localredirect.js @@ -0,0 +1,59 @@ +"use strict"; + +var prefs = Services.prefs; + +var nextTest; + +var listener = { + onLookupComplete(inRequest, inRecord, inStatus) { + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + + nextTest(); + do_test_finished(); + }, + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), +}; + +const defaultOriginAttributes = {}; + +function run_test() { + prefs.setCharPref("network.dns.localDomains", "local.vingtetun.org"); + + var mainThread = Services.tm.currentThread; + nextTest = do_test_2; + Services.dns.asyncResolve( + "local.vingtetun.org", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + + do_test_pending(); +} + +function do_test_2() { + var mainThread = Services.tm.currentThread; + nextTest = testsDone; + prefs.setCharPref("network.dns.forceResolve", "localhost"); + Services.dns.asyncResolve( + "www.example.com", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + + do_test_pending(); +} + +function testsDone() { + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.dns.forceResolve"); +} diff --git a/netwerk/test/unit/test_dns_offline.js b/netwerk/test/unit/test_dns_offline.js new file mode 100644 index 0000000000..db9c436292 --- /dev/null +++ b/netwerk/test/unit/test_dns_offline.js @@ -0,0 +1,105 @@ +"use strict"; + +var ioService = Services.io; +var prefs = Services.prefs; +var mainThread = Services.tm.currentThread; + +var listener1 = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_ERROR_OFFLINE); + test2(); + do_test_finished(); + }, +}; + +var listener2 = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_OK); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + test3(); + do_test_finished(); + }, +}; + +var listener3 = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_OK); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + cleanup(); + do_test_finished(); + }, +}; + +const defaultOriginAttributes = {}; + +function run_test() { + do_test_pending(); + prefs.setBoolPref("network.dns.offline-localhost", false); + // We always resolve localhost as it's hardcoded without the following pref: + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + ioService.offline = true; + try { + Services.dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener1, + mainThread, + defaultOriginAttributes + ); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_OFFLINE); + test2(); + do_test_finished(); + } +} + +function test2() { + do_test_pending(); + prefs.setBoolPref("network.dns.offline-localhost", true); + ioService.offline = false; + ioService.offline = true; + // we need to let the main thread run and apply the changes + do_timeout(0, test2Continued); +} + +function test2Continued() { + Services.dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener2, + mainThread, + defaultOriginAttributes + ); +} + +function test3() { + do_test_pending(); + ioService.offline = false; + // we need to let the main thread run and apply the changes + do_timeout(0, test3Continued); +} + +function test3Continued() { + Services.dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener3, + mainThread, + defaultOriginAttributes + ); +} + +function cleanup() { + prefs.clearUserPref("network.dns.offline-localhost"); + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); +} diff --git a/netwerk/test/unit/test_dns_onion.js b/netwerk/test/unit/test_dns_onion.js new file mode 100644 index 0000000000..928753f71f --- /dev/null +++ b/netwerk/test/unit/test_dns_onion.js @@ -0,0 +1,76 @@ +"use strict"; + +var mainThread = Services.tm.currentThread; + +var onionPref; +var localdomainPref; +var prefs = Services.prefs; + +// check that we don't lookup .onion +var listenerBlock = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.ok(!Components.isSuccessCode(inStatus)); + do_test_dontBlock(); + }, + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), +}; + +// check that we do lookup .onion (via pref) +var listenerDontBlock = { + onLookupComplete(inRequest, inRecord, inStatus) { + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + all_done(); + }, + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), +}; + +const defaultOriginAttributes = {}; + +function do_test_dontBlock() { + prefs.setBoolPref("network.dns.blockDotOnion", false); + Services.dns.asyncResolve( + "private.onion", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listenerDontBlock, + mainThread, + defaultOriginAttributes + ); +} + +function do_test_block() { + prefs.setBoolPref("network.dns.blockDotOnion", true); + try { + Services.dns.asyncResolve( + "private.onion", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listenerBlock, + mainThread, + defaultOriginAttributes + ); + } catch (e) { + // it is ok for this negative test to fail fast + Assert.ok(true); + do_test_dontBlock(); + } +} + +function all_done() { + // reset locally modified prefs + prefs.setCharPref("network.dns.localDomains", localdomainPref); + prefs.setBoolPref("network.dns.blockDotOnion", onionPref); + do_test_finished(); +} + +function run_test() { + onionPref = prefs.getBoolPref("network.dns.blockDotOnion"); + localdomainPref = prefs.getCharPref("network.dns.localDomains"); + prefs.setCharPref("network.dns.localDomains", "private.onion"); + do_test_block(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_dns_originAttributes.js b/netwerk/test/unit/test_dns_originAttributes.js new file mode 100644 index 0000000000..39e2a4f0f1 --- /dev/null +++ b/netwerk/test/unit/test_dns_originAttributes.js @@ -0,0 +1,93 @@ +"use strict"; + +var prefs = Services.prefs; +var mainThread = Services.tm.currentThread; + +var listener1 = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_OK); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + test2(); + do_test_finished(); + }, +}; + +var listener2 = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_OK); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + test3(); + do_test_finished(); + }, +}; + +var listener3 = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_ERROR_OFFLINE); + cleanup(); + do_test_finished(); + }, +}; + +const firstOriginAttributes = { userContextId: 1 }; +const secondOriginAttributes = { userContextId: 2 }; + +// First, we resolve the address normally for first originAttributes. +function run_test() { + do_test_pending(); + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + Services.dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener1, + mainThread, + firstOriginAttributes + ); +} + +// Second, we resolve the same address offline to see whether its DNS cache works +// correctly. +function test2() { + do_test_pending(); + Services.dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_OFFLINE, + null, // resolverInfo + listener2, + mainThread, + firstOriginAttributes + ); +} + +// Third, we resolve the same address offline again with different originAttributes. +// This resolving should fail since the DNS cache of the given address is not exist +// for this originAttributes. +function test3() { + do_test_pending(); + try { + Services.dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_OFFLINE, + null, // resolverInfo + listener3, + mainThread, + secondOriginAttributes + ); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_OFFLINE); + cleanup(); + do_test_finished(); + } +} + +function cleanup() { + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); +} diff --git a/netwerk/test/unit/test_dns_override.js b/netwerk/test/unit/test_dns_override.js new file mode 100644 index 0000000000..3dad511b4e --- /dev/null +++ b/netwerk/test/unit/test_dns_override.js @@ -0,0 +1,322 @@ +"use strict"; + +const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); +const defaultOriginAttributes = {}; +const mainThread = Services.tm.currentThread; + +class Listener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + + async firstAddress() { + let all = await this.addresses(); + if (all.length) { + return all[0]; + } + + return undefined; + } + + async addresses() { + let [, inRecord] = await this.promise; + let addresses = []; + if (!inRecord) { + return addresses; // returns [] + } + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + while (inRecord.hasMore()) { + addresses.push(inRecord.getNextAddrAsString()); + } + return addresses; + } + + then() { + return this.promise.then.apply(this.promise, arguments); + } +} +Listener.prototype.QueryInterface = ChromeUtils.generateQI(["nsIDNSListener"]); + +const DOMAIN = "example.org"; +const OTHER = "example.com"; + +add_task(async function test_bad_IPs() { + Assert.throws( + () => override.addIPOverride(DOMAIN, DOMAIN), + /NS_ERROR_UNEXPECTED/, + "Should throw if input is not an IP address" + ); + Assert.throws( + () => override.addIPOverride(DOMAIN, ""), + /NS_ERROR_UNEXPECTED/, + "Should throw if input is not an IP address" + ); + Assert.throws( + () => override.addIPOverride(DOMAIN, " "), + /NS_ERROR_UNEXPECTED/, + "Should throw if input is not an IP address" + ); + Assert.throws( + () => override.addIPOverride(DOMAIN, "1-2-3-4"), + /NS_ERROR_UNEXPECTED/, + "Should throw if input is not an IP address" + ); +}); + +add_task(async function test_ipv4() { + let listener = new Listener(); + override.addIPOverride(DOMAIN, "1.2.3.4"); + Services.dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.equal(await listener.firstAddress(), "1.2.3.4"); + + Services.dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_ipv6() { + let listener = new Listener(); + override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b"); + Services.dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.equal(await listener.firstAddress(), "fe80::6a99:9b2b:6ccc:6e1b"); + + Services.dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_clearOverrides() { + let listener = new Listener(); + override.addIPOverride(DOMAIN, "1.2.3.4"); + Services.dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.equal(await listener.firstAddress(), "1.2.3.4"); + + Services.dns.clearCache(false); + override.clearOverrides(); + + listener = new Listener(); + Services.dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.notEqual(await listener.firstAddress(), "1.2.3.4"); + + await new Promise(resolve => do_timeout(1000, resolve)); + Services.dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_clearHostOverride() { + override.addIPOverride(DOMAIN, "2.2.2.2"); + override.addIPOverride(OTHER, "2.2.2.2"); + override.clearHostOverride(DOMAIN); + let listener = new Listener(); + Services.dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + + Assert.notEqual(await listener.firstAddress(), "2.2.2.2"); + + listener = new Listener(); + Services.dns.asyncResolve( + OTHER, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.equal(await listener.firstAddress(), "2.2.2.2"); + + // Note: this test will use the actual system resolver. On windows we do a + // second async call to the system libraries to get the TTL values, which + // keeps the record alive after the onLookupComplete() + // We need to wait for a bit, until the second call is finished before we + // can clear the cache to make sure we evict everything. + // If the next task ever starts failing, with an IP that is not in this + // file, then likely the timeout is too small. + await new Promise(resolve => do_timeout(1000, resolve)); + Services.dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_multiple_IPs() { + override.addIPOverride(DOMAIN, "2.2.2.2"); + override.addIPOverride(DOMAIN, "1.1.1.1"); + override.addIPOverride(DOMAIN, "::1"); + override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b"); + let listener = new Listener(); + Services.dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.deepEqual(await listener.addresses(), [ + "2.2.2.2", + "1.1.1.1", + "::1", + "fe80::6a99:9b2b:6ccc:6e1b", + ]); + + Services.dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_address_family_flags() { + override.addIPOverride(DOMAIN, "2.2.2.2"); + override.addIPOverride(DOMAIN, "1.1.1.1"); + override.addIPOverride(DOMAIN, "::1"); + override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b"); + let listener = new Listener(); + Services.dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_DISABLE_IPV4, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.deepEqual(await listener.addresses(), [ + "::1", + "fe80::6a99:9b2b:6ccc:6e1b", + ]); + + listener = new Listener(); + Services.dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_DISABLE_IPV6, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.deepEqual(await listener.addresses(), ["2.2.2.2", "1.1.1.1"]); + + Services.dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_cname_flag() { + override.addIPOverride(DOMAIN, "2.2.2.2"); + let listener = new Listener(); + Services.dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + let [, inRecord] = await listener; + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + Assert.throws( + () => inRecord.canonicalName, + /NS_ERROR_NOT_AVAILABLE/, + "No canonical name flag" + ); + Assert.equal(inRecord.getNextAddrAsString(), "2.2.2.2"); + + listener = new Listener(); + Services.dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_CANONICAL_NAME, + null, + listener, + mainThread, + defaultOriginAttributes + ); + [, inRecord] = await listener; + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + Assert.equal(inRecord.canonicalName, DOMAIN, "No canonical name specified"); + Assert.equal(inRecord.getNextAddrAsString(), "2.2.2.2"); + + Services.dns.clearCache(false); + override.clearOverrides(); + + override.addIPOverride(DOMAIN, "2.2.2.2"); + override.setCnameOverride(DOMAIN, OTHER); + listener = new Listener(); + Services.dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_CANONICAL_NAME, + null, + listener, + mainThread, + defaultOriginAttributes + ); + [, inRecord] = await listener; + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + Assert.equal(inRecord.canonicalName, OTHER, "Must have correct CNAME"); + Assert.equal(inRecord.getNextAddrAsString(), "2.2.2.2"); + + Services.dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_nxdomain() { + override.addIPOverride(DOMAIN, "N/A"); + let listener = new Listener(); + Services.dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_CANONICAL_NAME, + null, + listener, + mainThread, + defaultOriginAttributes + ); + + let [, , inStatus] = await listener; + equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); +}); diff --git a/netwerk/test/unit/test_dns_override_for_localhost.js b/netwerk/test/unit/test_dns_override_for_localhost.js new file mode 100644 index 0000000000..ecd708ee53 --- /dev/null +++ b/netwerk/test/unit/test_dns_override_for_localhost.js @@ -0,0 +1,92 @@ +"use strict"; + +const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); +const defaultOriginAttributes = {}; +const mainThread = Services.tm.currentThread; + +class Listener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + + async addresses() { + let [, inRecord] = await this.promise; + let addresses = []; + if (!inRecord) { + return addresses; // returns [] + } + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + while (inRecord.hasMore()) { + addresses.push(inRecord.getNextAddrAsString()); + } + return addresses; + } + + then() { + return this.promise.then.apply(this.promise, arguments); + } +} +Listener.prototype.QueryInterface = ChromeUtils.generateQI(["nsIDNSListener"]); + +const DOMAINS = [ + "localhost", + "localhost.", + "vhost.localhost", + "vhost.localhost.", +]; +DOMAINS.forEach(domain => { + add_task(async function test_() { + let listener1 = new Listener(); + const overrides = ["1.2.3.4", "5.6.7.8"]; + overrides.forEach(ip_address => { + override.addIPOverride(domain, ip_address); + }); + + // Verify that loopback host names are not overridden. + Services.dns.asyncResolve( + domain, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener1, + mainThread, + defaultOriginAttributes + ); + Assert.deepEqual( + await listener1.addresses(), + ["127.0.0.1", "::1"], + `${domain} is not overridden` + ); + + // Verify that if localhost hijacking is enabled, the overrides + // registered above are taken into account. + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + let listener2 = new Listener(); + Services.dns.asyncResolve( + domain, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener2, + mainThread, + defaultOriginAttributes + ); + Assert.deepEqual( + await listener2.addresses(), + overrides, + `${domain} is overridden` + ); + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + + Services.dns.clearCache(false); + override.clearOverrides(); + }); +}); diff --git a/netwerk/test/unit/test_dns_proxy_bypass.js b/netwerk/test/unit/test_dns_proxy_bypass.js new file mode 100644 index 0000000000..d53efc3dd3 --- /dev/null +++ b/netwerk/test/unit/test_dns_proxy_bypass.js @@ -0,0 +1,95 @@ +/* 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"; + +var prefs = Services.prefs; + +function setup() { + prefs.setBoolPref("network.dns.notifyResolution", true); + prefs.setCharPref("network.proxy.socks", "127.0.0.1"); + prefs.setIntPref("network.proxy.socks_port", 9000); + prefs.setIntPref("network.proxy.type", 1); + prefs.setBoolPref("network.proxy.socks_remote_dns", true); +} + +setup(); +registerCleanupFunction(async () => { + prefs.clearUserPref("network.proxy.socks"); + prefs.clearUserPref("network.proxy.socks_port"); + prefs.clearUserPref("network.proxy.type"); + prefs.clearUserPref("network.proxy.socks_remote_dns"); + prefs.clearUserPref("network.dns.notifyResolution"); +}); + +var url = "ws://dnsleak.example.com"; + +var dnsRequestObserver = { + register() { + this.obs = Services.obs; + this.obs.addObserver(this, "dns-resolution-request"); + }, + + unregister() { + if (this.obs) { + this.obs.removeObserver(this, "dns-resolution-request"); + } + }, + + observe(subject, topic, data) { + if (topic == "dns-resolution-request") { + Assert.ok(!data.includes("dnsleak.example.com"), `no dnsleak: ${data}`); + } + }, +}; + +function WSListener(closure) { + this._closure = closure; +} +WSListener.prototype = { + onAcknowledge(aContext, aSize) {}, + onBinaryMessageAvailable(aContext, aMsg) {}, + onMessageAvailable(aContext, aMsg) {}, + onServerClose(aContext, aCode, aReason) {}, + onStart(aContext) {}, + onStop(aContext, aStatusCode) { + dnsRequestObserver.unregister(); + this._closure(); + }, +}; + +add_task(async function test_dns_websocket_channel() { + dnsRequestObserver.register(); + + var chan = Cc["@mozilla.org/network/protocol;1?name=ws"].createInstance( + Ci.nsIWebSocketChannel + ); + + var uri = Services.io.newURI(url); + chan.initLoadInfo( + null, // aLoadingNode + Services.scriptSecurityManager.createContentPrincipal(uri, {}), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_WEBSOCKET + ); + + await new Promise(resolve => + chan.asyncOpen(uri, url, {}, 0, new WSListener(resolve), null) + ); +}); + +add_task(async function test_dns_resolve_proxy() { + dnsRequestObserver.register(); + + let { error } = await new TRRDNSListener("dnsleak.example.com", { + expectEarlyFail: true, + }); + Assert.equal( + error.result, + Cr.NS_ERROR_UNKNOWN_PROXY_HOST, + "error is NS_ERROR_UNKNOWN_PROXY_HOST" + ); + dnsRequestObserver.unregister(); +}); diff --git a/netwerk/test/unit/test_dns_retry.js b/netwerk/test/unit/test_dns_retry.js new file mode 100644 index 0000000000..2ac313165d --- /dev/null +++ b/netwerk/test/unit/test_dns_retry.js @@ -0,0 +1,317 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +trr_test_setup(); +let httpServerIPv4 = new HttpServer(); +let httpServerIPv6 = new HttpServer(); +let trrServer; +let testpath = "/simple"; +let httpbody = "0123456789"; +let CC_IPV4 = "example_cc_ipv4.com"; +let CC_IPV6 = "example_cc_ipv6.com"; +Services.prefs.clearUserPref("network.dns.native-is-localhost"); + +XPCOMUtils.defineLazyGetter(this, "URL_CC_IPV4", function () { + return `http://${CC_IPV4}:${httpServerIPv4.identity.primaryPort}${testpath}`; +}); +XPCOMUtils.defineLazyGetter(this, "URL_CC_IPV6", function () { + return `http://${CC_IPV6}:${httpServerIPv6.identity.primaryPort}${testpath}`; +}); +XPCOMUtils.defineLazyGetter(this, "URL6a", function () { + return `http://example6a.com:${httpServerIPv6.identity.primaryPort}${testpath}`; +}); +XPCOMUtils.defineLazyGetter(this, "URL6b", function () { + return `http://example6b.com:${httpServerIPv6.identity.primaryPort}${testpath}`; +}); + +const ncs = Cc[ + "@mozilla.org/network/network-connectivity-service;1" +].getService(Ci.nsINetworkConnectivityService); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); + Services.prefs.clearUserPref("network.captive-portal-service.testMode"); + Services.prefs.clearUserPref("network.connectivity-service.IPv6.url"); + Services.prefs.clearUserPref("network.connectivity-service.IPv4.url"); + Services.prefs.clearUserPref("network.dns.localDomains"); + + trr_clear_prefs(); + await httpServerIPv4.stop(); + await httpServerIPv6.stop(); + await trrServer.stop(); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + chan.setTRRMode(Ci.nsIRequest.TRR_DEFAULT_MODE); + return chan; +} + +function serverHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(httpbody, httpbody.length); +} + +add_task(async function test_setup() { + httpServerIPv4.registerPathHandler(testpath, serverHandler); + httpServerIPv4.start(-1); + httpServerIPv6.registerPathHandler(testpath, serverHandler); + httpServerIPv6.start_ipv6(-1); + Services.prefs.setCharPref( + "network.dns.localDomains", + `foo.example.com, ${CC_IPV4}, ${CC_IPV6}` + ); + + trrServer = new TRRServer(); + await trrServer.start(); + + if (mozinfo.socketprocess_networking) { + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await registerDoHAnswers(true, true); +}); + +async function registerDoHAnswers(ipv4, ipv6) { + let hosts = ["example6a.com", "example6b.com"]; + for (const host of hosts) { + let ipv4answers = []; + if (ipv4) { + ipv4answers = [ + { + name: host, + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]; + } + await trrServer.registerDoHAnswers(host, "A", { + answers: ipv4answers, + }); + + let ipv6answers = []; + if (ipv6) { + ipv6answers = [ + { + name: host, + ttl: 55, + type: "AAAA", + flush: false, + data: "::1", + }, + ]; + } + + await trrServer.registerDoHAnswers(host, "AAAA", { + answers: ipv6answers, + }); + } + + Services.dns.clearCache(true); +} + +let StatusCounter = function () { + this._statusCount = {}; +}; +StatusCounter.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIProgressEventSink", + ]), + + getInterface(iid) { + return this.QueryInterface(iid); + }, + + onProgress(request, progress, progressMax) {}, + onStatus(request, status, statusArg) { + this._statusCount[status] = 1 + (this._statusCount[status] || 0); + }, +}; + +let HttpListener = function (finish, succeeded) { + this.finish = finish; + this.succeeded = succeeded; +}; + +HttpListener.prototype = { + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + equal(this.succeeded, status == Cr.NS_OK); + this.finish(); + }, +}; + +function promiseObserverNotification(aTopic, matchFunc) { + return new Promise((resolve, reject) => { + Services.obs.addObserver(function observe(subject, topic, data) { + let matches = typeof matchFunc != "function" || matchFunc(subject, data); + if (!matches) { + return; + } + Services.obs.removeObserver(observe, topic); + resolve({ subject, data }); + }, aTopic); + }); +} + +async function make_request(uri, check_events, succeeded) { + let chan = makeChan(uri); + let statusCounter = new StatusCounter(); + chan.notificationCallbacks = statusCounter; + await new Promise(resolve => + chan.asyncOpen(new HttpListener(resolve, succeeded)) + ); + + if (check_events) { + equal( + statusCounter._statusCount[0x4b000b] || 0, + 1, + "Expecting only one instance of NS_NET_STATUS_RESOLVED_HOST" + ); + equal( + statusCounter._statusCount[0x4b0007] || 0, + 1, + "Expecting only one instance of NS_NET_STATUS_CONNECTING_TO" + ); + } +} + +async function setup_connectivity(ipv6, ipv4) { + Services.prefs.setBoolPref("network.captive-portal-service.testMode", true); + + if (ipv6) { + Services.prefs.setCharPref( + "network.connectivity-service.IPv6.url", + URL_CC_IPV6 + testpath + ); + } else { + Services.prefs.setCharPref( + "network.connectivity-service.IPv6.url", + "http://donotexist.example.com" + ); + } + + if (ipv4) { + Services.prefs.setCharPref( + "network.connectivity-service.IPv4.url", + URL_CC_IPV4 + testpath + ); + } else { + Services.prefs.setCharPref( + "network.connectivity-service.IPv4.url", + "http://donotexist.example.com" + ); + } + + let topic = "network:connectivity-service:ip-checks-complete"; + if (mozinfo.socketprocess_networking) { + topic += "-from-socket-process"; + } + let observerNotification = promiseObserverNotification(topic); + ncs.recheckIPConnectivity(); + await observerNotification; + + if (!ipv6) { + equal( + ncs.IPv6, + Ci.nsINetworkConnectivityService.NOT_AVAILABLE, + "Check IPv6 support" + ); + } else { + equal(ncs.IPv6, Ci.nsINetworkConnectivityService.OK, "Check IPv6 support"); + } + + if (!ipv4) { + equal( + ncs.IPv4, + Ci.nsINetworkConnectivityService.NOT_AVAILABLE, + "Check IPv4 support" + ); + } else { + equal(ncs.IPv4, Ci.nsINetworkConnectivityService.OK, "Check IPv4 support"); + } +} + +// This test that we retry to connect using IPv4 when IPv6 connecivity is not +// present, but a ConnectionEntry have IPv6 prefered set. +// Speculative connections are disabled. +add_task(async function test_prefer_address_version_fail_trr3_1() { + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + await registerDoHAnswers(true, true); + + // Make a request to setup the address version preference to a ConnectionEntry. + await make_request(URL6a, true, true); + + // connect again using the address version preference from the ConnectionEntry. + await make_request(URL6a, true, true); + + // Make IPv6 connectivity check fail + await setup_connectivity(false, true); + + Services.dns.clearCache(true); + + // This will succeed as we query both DNS records + await make_request(URL6a, true, true); + + // Now make the DNS server only return IPv4 records + await registerDoHAnswers(true, false); + // This will fail, because the server is not lisenting to IPv4 address as well, + // We should still get NS_NET_STATUS_RESOLVED_HOST and + // NS_NET_STATUS_CONNECTING_TO notification. + await make_request(URL6a, true, false); + + // Make IPv6 connectivity check succeed again + await setup_connectivity(true, true); +}); + +// This test that we retry to connect using IPv4 when IPv6 connecivity is not +// present, but a ConnectionEntry have IPv6 prefered set. +// Speculative connections are enabled. +add_task(async function test_prefer_address_version_fail_trr3_2() { + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + await registerDoHAnswers(true, true); + + // Make a request to setup the address version preference to a ConnectionEntry. + await make_request(URL6b, false, true); + + // connect again using the address version preference from the ConnectionEntry. + await make_request(URL6b, false, true); + + // Make IPv6 connectivity check fail + await setup_connectivity(false, true); + + Services.dns.clearCache(true); + + // This will succeed as we query both DNS records + await make_request(URL6b, false, true); + + // Now make the DNS server only return IPv4 records + await registerDoHAnswers(true, false); + // This will fail, because the server is not lisenting to IPv4 address as well, + // We should still get NS_NET_STATUS_RESOLVED_HOST and + // NS_NET_STATUS_CONNECTING_TO notification. + await make_request(URL6b, true, false); +}); diff --git a/netwerk/test/unit/test_dns_service.js b/netwerk/test/unit/test_dns_service.js new file mode 100644 index 0000000000..c2a2d8b8ad --- /dev/null +++ b/netwerk/test/unit/test_dns_service.js @@ -0,0 +1,143 @@ +"use strict"; + +const defaultOriginAttributes = {}; +const mainThread = Services.tm.currentThread; + +const overrideService = Cc[ + "@mozilla.org/network/native-dns-override;1" +].getService(Ci.nsINativeDNSResolverOverride); + +class Listener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +Listener.prototype.QueryInterface = ChromeUtils.generateQI(["nsIDNSListener"]); + +const DOMAIN_IDN = "bücher.org"; +const ACE_IDN = "xn--bcher-kva.org"; + +const ADDR1 = "127.0.0.1"; +const ADDR2 = "::1"; + +add_task(async function test_dns_localhost() { + let listener = new Listener(); + Services.dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + let [, inRecord] = await listener; + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + let answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == ADDR1 || answer == ADDR2); +}); + +add_task(async function test_idn_cname() { + let listener = new Listener(); + Services.dns.asyncResolve( + DOMAIN_IDN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_CANONICAL_NAME, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + let [, inRecord] = await listener; + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + Assert.equal(inRecord.canonicalName, ACE_IDN, "IDN is returned as punycode"); +}); + +add_task( + { + skip_if: () => + Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT, + }, + async function test_long_domain() { + let listener = new Listener(); + let domain = "a".repeat(253); + overrideService.addIPOverride(domain, "1.2.3.4"); + Services.dns.asyncResolve( + domain, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_CANONICAL_NAME, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + let [, , inStatus] = await listener; + Assert.equal(inStatus, Cr.NS_OK); + + listener = new Listener(); + domain = "a".repeat(254); + overrideService.addIPOverride(domain, "1.2.3.4"); + + Services.prefs.setBoolPref("network.dns.limit_253_chars", true); + + if (mozinfo.socketprocess_networking) { + // When using the socket process, the call fails asynchronously. + Services.dns.asyncResolve( + domain, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_CANONICAL_NAME, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + let [, , inStatus] = await listener; + Assert.equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); + } else { + Assert.throws( + () => { + Services.dns.asyncResolve( + domain, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_CANONICAL_NAME, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + }, + /NS_ERROR_UNKNOWN_HOST/, + "Should throw for large domains" + ); + } + + listener = new Listener(); + domain = "a".repeat(254); + Services.prefs.setBoolPref("network.dns.limit_253_chars", false); + Services.dns.asyncResolve( + domain, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_CANONICAL_NAME, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + [, , inStatus] = await listener; + Assert.equal(inStatus, Cr.NS_OK); + + Services.prefs.clearUserPref("network.dns.limit_253_chars"); + overrideService.clearOverrides(); + } +); diff --git a/netwerk/test/unit/test_domain_eviction.js b/netwerk/test/unit/test_domain_eviction.js new file mode 100644 index 0000000000..58520c2daa --- /dev/null +++ b/netwerk/test/unit/test_domain_eviction.js @@ -0,0 +1,182 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that domain eviction occurs when the cookies per base domain limit is +// reached, and that expired cookies are evicted before live cookies. + +"use strict"; + +var test_generator = do_run_test(); + +function run_test() { + do_test_pending(); + do_run_generator(test_generator); +} + +function continue_test() { + do_run_generator(test_generator); +} + +function* do_run_test() { + // Set quotaPerHost to maxPerHost - 1, so there is only one cookie + // will be evicted everytime. + Services.prefs.setIntPref("network.cookie.quotaPerHost", 49); + // Set the base domain limit to 50 so we have a known value. + Services.prefs.setIntPref("network.cookie.maxPerHost", 50); + + let futureExpiry = Math.floor(Date.now() / 1000 + 1000); + + // test eviction under the 50 cookies per base domain limit. this means + // that cookies for foo.com and bar.foo.com should count toward this limit, + // while cookies for baz.com should not. there are several tests we perform + // to make sure the base domain logic is working correctly. + + // 1) simplest case: set 100 cookies for "foo.bar" and make sure 50 survive. + setCookies("foo.bar", 100, futureExpiry); + Assert.equal(countCookies("foo.bar", "foo.bar"), 50); + + // 2) set cookies for different subdomains of "foo.baz", and an unrelated + // domain, and make sure all 50 within the "foo.baz" base domain are counted. + setCookies("foo.baz", 10, futureExpiry); + setCookies(".foo.baz", 10, futureExpiry); + setCookies("bar.foo.baz", 10, futureExpiry); + setCookies("baz.bar.foo.baz", 10, futureExpiry); + setCookies("unrelated.domain", 50, futureExpiry); + Assert.equal(countCookies("foo.baz", "baz.bar.foo.baz"), 40); + setCookies("foo.baz", 20, futureExpiry); + Assert.equal(countCookies("foo.baz", "baz.bar.foo.baz"), 50); + + // 3) ensure cookies are evicted by order of lastAccessed time, if the + // limit on cookies per base domain is reached. + setCookies("horse.radish", 10, futureExpiry); + + // Wait a while, to make sure the first batch of cookies is older than + // the second (timer resolution varies on different platforms). + do_timeout(100, continue_test); + yield; + + setCookies("tasty.horse.radish", 50, futureExpiry); + Assert.equal(countCookies("horse.radish", "horse.radish"), 50); + + for (let cookie of Services.cookies.cookies) { + if (cookie.host == "horse.radish") { + do_throw("cookies not evicted by lastAccessed order"); + } + } + + // Test that expired cookies for a domain are evicted before live ones. + let shortExpiry = Math.floor(Date.now() / 1000 + 2); + setCookies("captchart.com", 49, futureExpiry); + Services.cookies.add( + "captchart.com", + "", + "test100", + "eviction", + false, + false, + false, + shortExpiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + do_timeout(2100, continue_test); + yield; + + Assert.equal(countCookies("captchart.com", "captchart.com"), 50); + Services.cookies.add( + "captchart.com", + "", + "test200", + "eviction", + false, + false, + false, + futureExpiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(countCookies("captchart.com", "captchart.com"), 50); + + for (let cookie of Services.cookies.getCookiesFromHost("captchart.com", {})) { + Assert.ok(cookie.expiry == futureExpiry); + } + + do_finish_generator_test(test_generator); +} + +// set 'aNumber' cookies with host 'aHost', with distinct names. +function setCookies(aHost, aNumber, aExpiry) { + for (let i = 0; i < aNumber; ++i) { + Services.cookies.add( + aHost, + "", + "test" + i, + "eviction", + false, + false, + false, + aExpiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + } +} + +// count how many cookies are within domain 'aBaseDomain', using three +// independent interface methods on nsICookieManager: +// 1) 'cookies', an array of all cookies; +// 2) 'countCookiesFromHost', which returns the number of cookies within the +// base domain of 'aHost', +// 3) 'getCookiesFromHost', which returns an array of 2). +function countCookies(aBaseDomain, aHost) { + // count how many cookies are within domain 'aBaseDomain' using the cookies + // array. + let cookies = []; + for (let cookie of Services.cookies.cookies) { + if ( + cookie.host.length >= aBaseDomain.length && + cookie.host.slice(cookie.host.length - aBaseDomain.length) == aBaseDomain + ) { + cookies.push(cookie); + } + } + + // confirm the count using countCookiesFromHost and getCookiesFromHost. + let result = cookies.length; + Assert.equal( + Services.cookies.countCookiesFromHost(aBaseDomain), + cookies.length + ); + Assert.equal(Services.cookies.countCookiesFromHost(aHost), cookies.length); + + for (let cookie of Services.cookies.getCookiesFromHost(aHost, {})) { + if ( + cookie.host.length >= aBaseDomain.length && + cookie.host.slice(cookie.host.length - aBaseDomain.length) == aBaseDomain + ) { + let found = false; + for (let i = 0; i < cookies.length; ++i) { + if (cookies[i].host == cookie.host && cookies[i].name == cookie.name) { + found = true; + cookies.splice(i, 1); + break; + } + } + + if (!found) { + do_throw("cookie " + cookie.name + " not found in master cookies"); + } + } else { + do_throw( + "cookie host " + cookie.host + " not within domain " + aBaseDomain + ); + } + } + + Assert.equal(cookies.length, 0); + + return result; +} diff --git a/netwerk/test/unit/test_dooh.js b/netwerk/test/unit/test_dooh.js new file mode 100644 index 0000000000..bbcdc5a377 --- /dev/null +++ b/netwerk/test/unit/test_dooh.js @@ -0,0 +1,356 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from trr_common.js */ + +Cu.importGlobalProperties(["fetch"]); + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +let httpServer; +let ohttpServer; +let ohttpEncodedConfig = "not a valid config"; + +// Decapsulate the request, send it to the actual TRR, receive the response, +// encapsulate it, and send it back through `response`. +async function forwardToTRR(request, response) { + let inputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + inputStream.init(request.bodyInputStream); + let requestBody = inputStream.readBytes(inputStream.available()); + let ohttpResponse = ohttpServer.decapsulate(stringToBytes(requestBody)); + let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService( + Ci.nsIBinaryHttp + ); + let decodedRequest = bhttp.decodeRequest(ohttpResponse.request); + let headers = {}; + for ( + let i = 0; + i < decodedRequest.headerNames.length && decodedRequest.headerValues.length; + i++ + ) { + headers[decodedRequest.headerNames[i]] = decodedRequest.headerValues[i]; + } + let uri = `${decodedRequest.scheme}://${decodedRequest.authority}${decodedRequest.path}`; + let body = new Uint8Array(decodedRequest.content.length); + for (let i = 0; i < decodedRequest.content.length; i++) { + body[i] = decodedRequest.content[i]; + } + try { + // Timeout after 10 seconds. + let fetchInProgress = true; + let controller = new AbortController(); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(() => { + if (fetchInProgress) { + controller.abort(); + } + }, 10000); + let trrResponse = await fetch(uri, { + method: decodedRequest.method, + headers, + body: decodedRequest.method == "POST" ? body : undefined, + credentials: "omit", + signal: controller.signal, + }); + fetchInProgress = false; + let data = new Uint8Array(await trrResponse.arrayBuffer()); + let trrResponseContent = []; + for (let i = 0; i < data.length; i++) { + trrResponseContent.push(data[i]); + } + let trrResponseHeaderNames = []; + let trrResponseHeaderValues = []; + for (let header of trrResponse.headers) { + trrResponseHeaderNames.push(header[0]); + trrResponseHeaderValues.push(header[1]); + } + let binaryResponse = new BinaryHttpResponse( + trrResponse.status, + trrResponseHeaderNames, + trrResponseHeaderValues, + trrResponseContent + ); + let responseBytes = bhttp.encodeResponse(binaryResponse); + let encResponse = ohttpResponse.encapsulate(responseBytes); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "message/ohttp-res", false); + response.write(bytesToString(encResponse)); + } catch (e) { + // Some tests involve the responder either timing out or closing the + // connection unexpectedly. + } +} + +add_setup(async function setup() { + h2Port = trr_test_setup(); + runningOHTTPTests = true; + + if (mozinfo.socketprocess_networking) { + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } + + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY); + + let ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService( + Ci.nsIObliviousHttp + ); + ohttpServer = ohttp.server(); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/relay", function (request, response) { + response.processAsync(); + forwardToTRR(request, response).then(() => { + response.finish(); + }); + }); + httpServer.registerPathHandler("/config", function (request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/ohttp-keys", false); + response.write(ohttpEncodedConfig); + }); + httpServer.start(-1); + + Services.prefs.setBoolPref("network.trr.use_ohttp", true); + // On windows the TTL fetch will race with clearing the cache + // to refresh the cache entry. + Services.prefs.setBoolPref("network.dns.get-ttl", false); + + registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.trr.use_ohttp"); + Services.prefs.clearUserPref("network.trr.ohttp.config_uri"); + Services.prefs.clearUserPref("network.trr.ohttp.relay_uri"); + Services.prefs.clearUserPref("network.trr.ohttp.uri"); + Services.prefs.clearUserPref("network.dns.get-ttl"); + await new Promise((resolve, reject) => { + httpServer.stop(resolve); + }); + }); +}); + +// Test that if DNS-over-OHTTP isn't configured, the implementation falls back +// to platform resolution. +add_task(async function test_ohttp_not_configured() { + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + await new TRRDNSListener("example.com", "127.0.0.1"); +}); + +add_task(async function set_ohttp_invalid_prefs() { + let configPromise = TestUtils.topicObserved("ohttp-service-config-loaded"); + Services.prefs.setCharPref( + "network.trr.ohttp.relay_uri", + "http://nonexistent.test" + ); + Services.prefs.setCharPref( + "network.trr.ohttp.config_uri", + "http://nonexistent.test" + ); + + Cc["@mozilla.org/network/oblivious-http-service;1"].getService( + Ci.nsIObliviousHttpService + ); + await configPromise; +}); + +// Test that if DNS-over-OHTTP has an invalid configuration, the implementation +// falls back to platform resolution. +add_task(async function test_ohttp_invalid_prefs_fallback() { + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + await new TRRDNSListener("example.com", "127.0.0.1"); +}); + +add_task(async function set_ohttp_prefs_500_error() { + let configPromise = TestUtils.topicObserved("ohttp-service-config-loaded"); + Services.prefs.setCharPref( + "network.trr.ohttp.relay_uri", + `http://localhost:${httpServer.identity.primaryPort}/relay` + ); + Services.prefs.setCharPref( + "network.trr.ohttp.config_uri", + `http://localhost:${httpServer.identity.primaryPort}/500error` + ); + await configPromise; +}); + +// Test that if DNS-over-OHTTP has an invalid configuration, the implementation +// falls back to platform resolution. +add_task(async function test_ohttp_500_error_fallback() { + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + await new TRRDNSListener("example.com", "127.0.0.1"); +}); + +add_task(async function retryConfigOnConnectivityChange() { + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + // First we make sure the config is properly loaded + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + let ohttpService = Cc[ + "@mozilla.org/network/oblivious-http-service;1" + ].getService(Ci.nsIObliviousHttpService); + ohttpService.clearTRRConfig(); + ohttpEncodedConfig = bytesToString(ohttpServer.encodedConfig); + let configPromise = TestUtils.topicObserved("ohttp-service-config-loaded"); + Services.prefs.setCharPref( + "network.trr.ohttp.relay_uri", + `http://localhost:${httpServer.identity.primaryPort}/relay` + ); + Services.prefs.setCharPref( + "network.trr.ohttp.config_uri", + `http://localhost:${httpServer.identity.primaryPort}/config` + ); + let [, status] = await configPromise; + equal(status, "success"); + info("retryConfigOnConnectivityChange setup complete"); + + ohttpService.clearTRRConfig(); + + let port = httpServer.identity.primaryPort; + // Stop the server so getting the config fails. + await httpServer.stop(); + + configPromise = TestUtils.topicObserved("ohttp-service-config-loaded"); + Services.obs.notifyObservers( + null, + "network:captive-portal-connectivity-changed" + ); + [, status] = await configPromise; + equal(status, "failed"); + + // Should fallback to native DNS since the config is empty + Services.dns.clearCache(true); + await new TRRDNSListener("example.com", "127.0.0.1"); + + // Start the server back again. + httpServer.start(port); + Assert.equal( + port, + httpServer.identity.primaryPort, + "server should get the same port" + ); + + // Still the config hasn't been reloaded. + await new TRRDNSListener("example2.com", "127.0.0.1"); + + // Signal a connectivity change so we reload the config + configPromise = TestUtils.topicObserved("ohttp-service-config-loaded"); + Services.obs.notifyObservers( + null, + "network:captive-portal-connectivity-changed" + ); + [, status] = await configPromise; + equal(status, "success"); + + await new TRRDNSListener("example3.com", "2.2.2.2"); + + // Now check that we also reload a missing config if a TRR confirmation fails. + ohttpService.clearTRRConfig(); + configPromise = TestUtils.topicObserved("ohttp-service-config-loaded"); + Services.obs.notifyObservers( + null, + "network:trr-confirmation", + "CONFIRM_FAILED" + ); + [, status] = await configPromise; + equal(status, "success"); + await new TRRDNSListener("example4.com", "2.2.2.2"); + + // set the config to an invalid value and check that as long as the URL + // doesn't change, we dont reload it again on connectivity notifications. + ohttpEncodedConfig = "not a valid config"; + configPromise = TestUtils.topicObserved("ohttp-service-config-loaded"); + Services.obs.notifyObservers( + null, + "network:captive-portal-connectivity-changed" + ); + + await new TRRDNSListener("example5.com", "2.2.2.2"); + + // The change should not cause any config reload because we already have a config. + [, status] = await configPromise; + equal(status, "no-changes"); + + await new TRRDNSListener("example6.com", "2.2.2.2"); + // Clear the config_uri pref so it gets set to the proper value in the next test. + configPromise = TestUtils.topicObserved("ohttp-service-config-loaded"); + Services.prefs.setCharPref("network.trr.ohttp.config_uri", ``); + await configPromise; +}); + +add_task(async function set_ohttp_prefs_valid() { + let ohttpService = Cc[ + "@mozilla.org/network/oblivious-http-service;1" + ].getService(Ci.nsIObliviousHttpService); + ohttpService.clearTRRConfig(); + let configPromise = TestUtils.topicObserved("ohttp-service-config-loaded"); + ohttpEncodedConfig = bytesToString(ohttpServer.encodedConfig); + Services.prefs.setCharPref( + "network.trr.ohttp.config_uri", + `http://localhost:${httpServer.identity.primaryPort}/config` + ); + await configPromise; +}); + +add_task(test_A_record); + +add_task(test_AAAA_records); + +add_task(test_RFC1918); + +add_task(test_GET_ECS); + +add_task(test_timeout_mode3); + +add_task(test_strict_native_fallback); + +add_task(test_no_answers_fallback); + +add_task(test_404_fallback); + +add_task(test_mode_1_and_4); + +add_task(test_CNAME); + +add_task(test_name_mismatch); + +add_task(test_mode_2); + +add_task(test_excluded_domains); + +add_task(test_captiveportal_canonicalURL); + +add_task(test_parentalcontrols); + +// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref +add_task(test_builtin_excluded_domains); + +add_task(test_excluded_domains_mode3); + +add_task(test25e); + +add_task(test_parentalcontrols_mode3); + +add_task(test_builtin_excluded_domains_mode3); + +add_task(count_cookies); + +// This test doesn't work with having a JS httpd server as a relay. +// add_task(test_connection_closed); + +add_task(test_fetch_time); + +add_task(test_fqdn); + +add_task(test_ipv6_trr_fallback); + +add_task(test_ipv4_trr_fallback); + +add_task(test_no_retry_without_doh); diff --git a/netwerk/test/unit/test_doomentry.js b/netwerk/test/unit/test_doomentry.js new file mode 100644 index 0000000000..2ad0ee5525 --- /dev/null +++ b/netwerk/test/unit/test_doomentry.js @@ -0,0 +1,108 @@ +/** + * Test for nsICacheStorage.asyncDoomURI(). + * It tests dooming + * - an existent inactive entry + * - a non-existent inactive entry + * - an existent active entry + */ + +"use strict"; + +function doom(url, callback) { + Services.cache2 + .diskCacheStorage(Services.loadContextInfo.default) + .asyncDoomURI(createURI(url), "", { + onCacheEntryDoomed(result) { + callback(result); + }, + }); +} + +function write_and_check(str, data, len) { + var written = str.write(data, len); + if (written != len) { + do_throw( + "str.write has not written all data!\n" + + " Expected: " + + len + + "\n" + + " Actual: " + + written + + "\n" + ); + } +} + +function write_entry() { + asyncOpenCacheEntry( + "http://testentry/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + null, + function (status, entry) { + write_entry_cont(entry, entry.openOutputStream(0, -1)); + } + ); +} + +function write_entry_cont(entry, ostream) { + var data = "testdata"; + write_and_check(ostream, data, data.length); + ostream.close(); + entry.close(); + doom("http://testentry/", check_doom1); +} + +function check_doom1(status) { + Assert.equal(status, Cr.NS_OK); + doom("http://nonexistententry/", check_doom2); +} + +function check_doom2(status) { + Assert.equal(status, Cr.NS_ERROR_NOT_AVAILABLE); + asyncOpenCacheEntry( + "http://testentry/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + null, + function (status, entry) { + write_entry2(entry, entry.openOutputStream(0, -1)); + } + ); +} + +var gEntry; +var gOstream; +function write_entry2(entry, ostream) { + // write some data and doom the entry while it is active + var data = "testdata"; + write_and_check(ostream, data, data.length); + gEntry = entry; + gOstream = ostream; + doom("http://testentry/", check_doom3); +} + +function check_doom3(status) { + Assert.equal(status, Cr.NS_OK); + // entry was doomed but writing should still succeed + var data = "testdata"; + write_and_check(gOstream, data, data.length); + gOstream.close(); + gEntry.close(); + // dooming the same entry again should fail + doom("http://testentry/", check_doom4); +} + +function check_doom4(status) { + Assert.equal(status, Cr.NS_ERROR_NOT_AVAILABLE); + do_test_finished(); +} + +function run_test() { + do_get_profile(); + + // clear the cache + evict_cache_entries(); + write_entry(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_duplicate_headers.js b/netwerk/test/unit/test_duplicate_headers.js new file mode 100644 index 0000000000..9db7d95976 --- /dev/null +++ b/netwerk/test/unit/test_duplicate_headers.js @@ -0,0 +1,561 @@ +/* + * Tests bugs 597706, 655389: prevent duplicate headers with differing values + * for some headers like Content-Length, Location, etc. + */ + +//////////////////////////////////////////////////////////////////////////////// +// Test infrastructure + +"use strict"; + +// The tests in this file use number indexes to run, which can't be detected +// via ESLint. +/* eslint-disable no-unused-vars */ + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var test_flags = []; +var testPathBase = "/dupe_hdrs"; + +function run_test() { + httpserver.start(-1); + + do_test_pending(); + run_test_number(1); +} + +function run_test_number(num) { + let testPath = testPathBase + num; + httpserver.registerPathHandler(testPath, globalThis["handler" + num]); + + var channel = setupChannel(testPath); + let flags = test_flags[num]; // OK if flags undefined for test + channel.asyncOpen( + new ChannelListener(globalThis["completeTest" + num], channel, flags) + ); +} + +function setupChannel(url) { + var chan = NetUtil.newChannel({ + uri: URL + url, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + return httpChan; +} + +function endTests() { + httpserver.stop(do_test_finished); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 1: FAIL because of conflicting Content-Length headers +test_flags[1] = CL_EXPECT_FAILURE; + +function handler1(metadata, response) { + var body = "012345678901234567890123456789"; + // Comrades! We must seize power from the petty-bourgeois running dogs of + // httpd.js in order to reply with multiple instances of the same header! + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Content-Length: 20\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest1(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(2); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 2: OK to have duplicate same Content-Length headers + +function handler2(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest2(request, data, ctx) { + Assert.equal(request.status, 0); + run_test_number(3); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 3: FAIL: 2nd Content-length is blank +test_flags[3] = CL_EXPECT_FAILURE; + +function handler3(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Content-Length:\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest3(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(4); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 4: ensure that blank C-len header doesn't allow attacker to reset Clen, +// then insert CRLF attack +test_flags[4] = CL_EXPECT_FAILURE; + +function handler4(metadata, response) { + var body = "012345678901234567890123456789"; + + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + + // Bad Mr Hacker! Bad! + var evilBody = "We are the Evil bytes, Evil bytes, Evil bytes!"; + response.write("Content-Length:\r\n"); + response.write("Content-Length: %s\r\n\r\n%s" % (evilBody.length, evilBody)); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest4(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(5); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 5: ensure that we take 1st instance of duplicate, nonmerged headers that +// are permitted : (ex: Referrer) + +function handler5(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Referer: naive.org\r\n"); + response.write("Referer: evil.net\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest5(request, data, ctx) { + try { + let referer = request.getResponseHeader("Referer"); + Assert.equal(referer, "naive.org"); + } catch (ex) { + do_throw("Referer header should be present"); + } + + run_test_number(6); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 5: FAIL if multiple, different Location: headers present +// - needed to prevent CRLF injection attacks +test_flags[6] = CL_EXPECT_FAILURE; + +function handler6(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 301 Moved\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Location: " + URL + "/content\r\n"); + response.write("Location: http://www.microsoft.com/\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest6(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + // run_test_number(7); // Test 7 leaking under e10s: unrelated bug? + run_test_number(8); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 7: OK to have multiple Location: headers with same value + +function handler7(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 301 Moved\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + // redirect to previous test handler that completes OK: test 5 + response.write("Location: " + URL + testPathBase + "5\r\n"); + response.write("Location: " + URL + testPathBase + "5\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest7(request, data, ctx) { + // for some reason need this here + request.QueryInterface(Ci.nsIHttpChannel); + + try { + let referer = request.getResponseHeader("Referer"); + Assert.equal(referer, "naive.org"); + } catch (ex) { + do_throw("Referer header should be present"); + } + + run_test_number(8); +} + +//////////////////////////////////////////////////////////////////////////////// +// FAIL if 2nd Location: headers blank +test_flags[8] = CL_EXPECT_FAILURE; + +function handler8(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 301 Moved\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + // redirect to previous test handler that completes OK: test 4 + response.write("Location: " + URL + testPathBase + "4\r\n"); + response.write("Location:\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest8(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(9); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 9: ensure that blank Location header doesn't allow attacker to reset, +// then insert an evil one +test_flags[9] = CL_EXPECT_FAILURE; + +function handler9(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 301 Moved\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + // redirect to previous test handler that completes OK: test 2 + response.write("Location: " + URL + testPathBase + "2\r\n"); + response.write("Location:\r\n"); + // redirect to previous test handler that completes OK: test 4 + response.write("Location: " + URL + testPathBase + "4\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest9(request, data, ctx) { + // All redirection should fail: + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(10); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 10: FAIL: if conflicting values for Content-Dispo +test_flags[10] = CL_EXPECT_FAILURE; + +function handler10(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Content-Disposition: attachment; filename=foo\r\n"); + response.write("Content-Disposition: attachment; filename=bar\r\n"); + response.write("Content-Disposition: attachment; filename=baz\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest10(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(11); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 11: OK to have duplicate same Content-Disposition headers + +function handler11(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Content-Disposition: attachment; filename=foo\r\n"); + response.write("Content-Disposition: attachment; filename=foo\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest11(request, data, ctx) { + Assert.equal(request.status, 0); + + try { + var chan = request.QueryInterface(Ci.nsIChannel); + Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); + Assert.equal(chan.contentDispositionFilename, "foo"); + Assert.equal(chan.contentDispositionHeader, "attachment; filename=foo"); + } catch (ex) { + do_throw("error parsing Content-Disposition: " + ex); + } + + run_test_number(12); +} + +//////////////////////////////////////////////////////////////////////////////// +// Bug 716801 OK for Location: header to be blank + +function handler12(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Location:\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest12(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(30, data.length); + + run_test_number(13); +} + +//////////////////////////////////////////////////////////////////////////////// +// Negative content length is ok +test_flags[13] = CL_ALLOW_UNKNOWN_CL; + +function handler13(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: -1\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest13(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(30, data.length); + + run_test_number(14); +} + +//////////////////////////////////////////////////////////////////////////////// +// leading negative content length is not ok if paired with positive one + +test_flags[14] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL; + +function handler14(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: -1\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest14(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(15); +} + +//////////////////////////////////////////////////////////////////////////////// +// trailing negative content length is not ok if paired with positive one + +test_flags[15] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL; + +function handler15(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Content-Length: -1\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest15(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(16); +} + +//////////////////////////////////////////////////////////////////////////////// +// empty content length is ok +test_flags[16] = CL_ALLOW_UNKNOWN_CL; +let reran16 = false; + +function handler16(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: \r\n"); + response.write("Cache-Control: max-age=600\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest16(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(30, data.length); + + if (!reran16) { + reran16 = true; + run_test_number(16); + } else { + run_test_number(17); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// empty content length paired with non empty is not ok +test_flags[17] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL; + +function handler17(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: \r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest17(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(18); +} + +//////////////////////////////////////////////////////////////////////////////// +// alpha content-length is just like -1 +test_flags[18] = CL_ALLOW_UNKNOWN_CL; + +function handler18(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: seventeen\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest18(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(30, data.length); + + run_test_number(19); +} + +//////////////////////////////////////////////////////////////////////////////// +// semi-colons are ok too in the content-length +test_flags[19] = CL_ALLOW_UNKNOWN_CL; + +function handler19(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30;\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest19(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(30, data.length); + + run_test_number(20); +} + +//////////////////////////////////////////////////////////////////////////////// +// FAIL if 1st Location: header is blank, followed by non-blank +test_flags[20] = CL_EXPECT_FAILURE; + +function handler20(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 301 Moved\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + // redirect to previous test handler that completes OK: test 4 + response.write("Location:\r\n"); + response.write("Location: " + URL + testPathBase + "4\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest20(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + endTests(); +} diff --git a/netwerk/test/unit/test_early_hint_listener.js b/netwerk/test/unit/test_early_hint_listener.js new file mode 100644 index 0000000000..a0db7190fe --- /dev/null +++ b/netwerk/test/unit/test_early_hint_listener.js @@ -0,0 +1,168 @@ +/* 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 { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var earlyhintspath = "/earlyhints"; +var multipleearlyhintspath = "/multipleearlyhintspath"; +var otherearlyhintspath = "/otherearlyhintspath"; +var noearlyhintspath = "/noearlyhints"; +var httpbody = "0123456789"; +var hint1 = "</style.css>; rel=preload; as=style"; +var hint2 = "</img.png>; rel=preload; as=image"; + +function earlyHintsResponse(metadata, response) { + response.setInformationalResponseStatusLine( + metadata.httpVersion, + 103, + "EarlyHints" + ); + response.setInformationalResponseHeader("Link", hint1, false); + + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(httpbody, httpbody.length); +} + +function multipleEarlyHintsResponse(metadata, response) { + response.setInformationalResponseStatusLine( + metadata.httpVersion, + 103, + "EarlyHints" + ); + response.setInformationalResponseHeader("Link", hint1, false); + response.setInformationalHeaderNoCheck("Link", hint2); + + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(httpbody, httpbody.length); +} + +function otherHeadersEarlyHintsResponse(metadata, response) { + response.setInformationalResponseStatusLine( + metadata.httpVersion, + 103, + "EarlyHints" + ); + response.setInformationalResponseHeader("Link", hint1, false); + response.setInformationalResponseHeader("Something", "something", false); + + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(httpbody, httpbody.length); +} + +function noEarlyHintsResponse(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(httpbody, httpbody.length); +} + +let EarlyHintsListener = function () {}; + +EarlyHintsListener.prototype = { + _expected_hints: "", + earlyHintsReceived: false, + + earlyHint: function testEarlyHint(header) { + Assert.equal(header, this._expected_hints); + this.earlyHintsReceived = true; + }, +}; + +function chanPromise(uri, listener) { + return new Promise(resolve => { + var principal = Services.scriptSecurityManager.createContentPrincipal( + NetUtil.newURI(uri), + {} + ); + var chan = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + chan + .QueryInterface(Ci.nsIHttpChannelInternal) + .setEarlyHintObserver(listener); + chan.asyncOpen(new ChannelListener(resolve)); + }); +} + +add_task(async function setup() { + httpserver.registerPathHandler(earlyhintspath, earlyHintsResponse); + httpserver.registerPathHandler( + multipleearlyhintspath, + multipleEarlyHintsResponse + ); + httpserver.registerPathHandler( + otherearlyhintspath, + otherHeadersEarlyHintsResponse + ); + httpserver.registerPathHandler(noearlyhintspath, noEarlyHintsResponse); + httpserver.start(-1); +}); + +add_task(async function early_hints() { + let earlyHints = new EarlyHintsListener(); + earlyHints._expected_hints = hint1; + + await chanPromise( + "http://localhost:" + httpserver.identity.primaryPort + earlyhintspath, + earlyHints + ); + Assert.ok(earlyHints.earlyHintsReceived); +}); + +add_task(async function no_early_hints() { + let earlyHints = new EarlyHintsListener(""); + + await chanPromise( + "http://localhost:" + httpserver.identity.primaryPort + noearlyhintspath, + earlyHints + ); + Assert.ok(!earlyHints.earlyHintsReceived); +}); + +add_task(async function early_hints_multiple() { + let earlyHints = new EarlyHintsListener(); + earlyHints._expected_hints = hint1 + ", " + hint2; + + await chanPromise( + "http://localhost:" + + httpserver.identity.primaryPort + + multipleearlyhintspath, + earlyHints + ); + Assert.ok(earlyHints.earlyHintsReceived); +}); + +add_task(async function early_hints_other() { + let earlyHints = new EarlyHintsListener(); + earlyHints._expected_hints = hint1; + + await chanPromise( + "http://localhost:" + httpserver.identity.primaryPort + otherearlyhintspath, + earlyHints + ); + Assert.ok(earlyHints.earlyHintsReceived); +}); + +add_task(async function early_hints_only_secure_context() { + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + let earlyHints2 = new EarlyHintsListener(); + earlyHints2._expected_hints = ""; + + await chanPromise( + "http://localhost:" + httpserver.identity.primaryPort + earlyhintspath, + earlyHints2 + ); + Assert.ok(!earlyHints2.earlyHintsReceived); +}); + +add_task(async function clean_up() { + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + await new Promise(resolve => { + httpserver.stop(resolve); + }); +}); diff --git a/netwerk/test/unit/test_early_hint_listener_http2.js b/netwerk/test/unit/test_early_hint_listener_http2.js new file mode 100644 index 0000000000..3ea32f0bcc --- /dev/null +++ b/netwerk/test/unit/test_early_hint_listener_http2.js @@ -0,0 +1,110 @@ +/* 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"; + +// The server will always respond with a 103 EarlyHint followed by a +// 200 response. +// 103 response contains: +// 1) a non-link header +// 2) a link header if a request has a "link-to-set" header. If the +// request header is not set, the response will not have Link headers. +// A "link-to-set" header may contain multiple link headers +// separated with a comma. + +var earlyhintspath = "/103_response"; +var hint1 = "</style.css>; rel=preload; as=style"; +var hint2 = "</img.png>; rel=preload; as=image"; + +let EarlyHintsListener = function () {}; + +EarlyHintsListener.prototype = { + _expected_hints: "", + earlyHintsReceived: false, + + QueryInterface: ChromeUtils.generateQI(["nsIEarlyHintObserver"]), + + earlyHint: function testEarlyHint(header) { + Assert.equal(header, this._expected_hints); + this.earlyHintsReceived = true; + }, +}; + +function chanPromise(uri, listener, headers) { + return new Promise(resolve => { + var principal = Services.scriptSecurityManager.createContentPrincipal( + NetUtil.newURI(uri), + {} + ); + var chan = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + chan + .QueryInterface(Ci.nsIHttpChannel) + .setRequestHeader("link-to-set", headers, false); + chan + .QueryInterface(Ci.nsIHttpChannelInternal) + .setEarlyHintObserver(listener); + chan.asyncOpen(new ChannelListener(resolve)); + }); +} + +let http2Port; + +add_task(async function setup() { + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + http2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(http2Port, null); + Assert.notEqual(http2Port, ""); + + do_get_profile(); + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); +}); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.dns.localDomains"); +}); + +add_task(async function early_hints() { + let earlyHints = new EarlyHintsListener(); + earlyHints._expected_hints = hint1; + + await chanPromise( + `https://foo.example.com:${http2Port}${earlyhintspath}`, + earlyHints, + hint1 + ); + Assert.ok(earlyHints.earlyHintsReceived); +}); + +// Test when there is no Link header in a 103 response. +// 103 response will contain non-link headers. +add_task(async function no_early_hints() { + let earlyHints = new EarlyHintsListener(""); + + await chanPromise( + `https://foo.example.com:${http2Port}${earlyhintspath}`, + earlyHints, + "" + ); + Assert.ok(!earlyHints.earlyHintsReceived); +}); + +add_task(async function early_hints_multiple() { + let earlyHints = new EarlyHintsListener(); + earlyHints._expected_hints = hint1 + ", " + hint2; + + await chanPromise( + `https://foo.example.com:${http2Port}${earlyhintspath}`, + earlyHints, + hint1 + ", " + hint2 + ); + Assert.ok(earlyHints.earlyHintsReceived); +}); diff --git a/netwerk/test/unit/test_ech_grease.js b/netwerk/test/unit/test_ech_grease.js new file mode 100644 index 0000000000..bbe9c027b5 --- /dev/null +++ b/netwerk/test/unit/test_ech_grease.js @@ -0,0 +1,270 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +"use strict"; + +// Allow telemetry probes which may otherwise be disabled for some +// applications (e.g. Thunderbird). +Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true +); + +// Get a profile directory and ensure PSM initializes NSS. +do_get_profile(); +Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + +class InputStreamCallback { + constructor(output) { + this.output = output; + this.stopped = false; + } + + onInputStreamReady(stream) { + info("input stream ready"); + if (this.stopped) { + info("input stream callback stopped - bailing"); + return; + } + let available = 0; + try { + available = stream.available(); + } catch (e) { + // onInputStreamReady may fire when the stream has been closed. + equal( + e.result, + Cr.NS_BASE_STREAM_CLOSED, + "error should be NS_BASE_STREAM_CLOSED" + ); + } + if (available > 0) { + let request = NetUtil.readInputStreamToString(stream, available, { + charset: "utf8", + }); + ok( + request.startsWith("GET / HTTP/1.1\r\n"), + "Should get a simple GET / HTTP/1.1 request" + ); + let response = + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 2\r\n" + + "Content-Type: text/plain\r\n" + + "\r\nOK"; + let written = this.output.write(response, response.length); + equal( + written, + response.length, + "should have been able to write entire response" + ); + } + this.output.close(); + info("done with input stream ready"); + } + + stop() { + this.stopped = true; + this.output.close(); + } +} + +class TLSServerSecurityObserver { + constructor(input, output) { + this.input = input; + this.output = output; + this.callbacks = []; + this.stopped = false; + } + + onHandshakeDone(socket, status) { + info("TLS handshake done"); + info(`TLS version used: ${status.tlsVersionUsed}`); + + if (this.stopped) { + info("handshake done callback stopped - bailing"); + return; + } + + let callback = new InputStreamCallback(this.output); + this.callbacks.push(callback); + this.input.asyncWait(callback, 0, 0, Services.tm.currentThread); + } + + stop() { + this.stopped = true; + this.input.close(); + this.output.close(); + this.callbacks.forEach(callback => { + callback.stop(); + }); + } +} + +class ServerSocketListener { + constructor() { + this.securityObservers = []; + } + + onSocketAccepted(socket, transport) { + info("accepted TLS client connection"); + let connectionInfo = transport.securityCallbacks.getInterface( + Ci.nsITLSServerConnectionInfo + ); + let input = transport.openInputStream(0, 0, 0); + let output = transport.openOutputStream(0, 0, 0); + let securityObserver = new TLSServerSecurityObserver(input, output); + this.securityObservers.push(securityObserver); + connectionInfo.setSecurityObserver(securityObserver); + } + + // For some reason we get input stream callback events after we've stopped + // listening, so this ensures we just drop those events. + onStopListening() { + info("onStopListening"); + this.securityObservers.forEach(observer => { + observer.stop(); + }); + } +} + +function startServer( + minServerVersion = Ci.nsITLSClientStatus.TLS_VERSION_1_2, + maxServerVersion = Ci.nsITLSClientStatus.TLS_VERSION_1_3 +) { + let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance( + Ci.nsITLSServerSocket + ); + tlsServer.init(-1, true, -1); + tlsServer.serverCert = getTestServerCertificate(); + tlsServer.setVersionRange(minServerVersion, maxServerVersion); + tlsServer.setSessionTickets(false); + tlsServer.asyncListen(new ServerSocketListener()); + storeCertOverride(tlsServer.port, tlsServer.serverCert); + return tlsServer; +} + +const hostname = "example.com"; + +function storeCertOverride(port, cert) { + let certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" + ].getService(Ci.nsICertOverrideService); + certOverrideService.rememberValidityOverride(hostname, port, {}, cert, true); +} + +function startClient(port, useGREASE, beConservative) { + HandshakeTelemetryHelpers.resetHistograms(); + let flavors = ["", "_FIRST_TRY"]; + let nonflavors = ["_ECH"]; + + if (useGREASE) { + Services.prefs.setIntPref("security.tls.ech.grease_probability", 100); + } else { + Services.prefs.setIntPref("security.tls.ech.grease_probability", 0); + } + + let req = new XMLHttpRequest(); + req.open("GET", `https://${hostname}:${port}`); + + if (beConservative) { + // We don't have a way to set DONT_TRY_ECH at the moment. + let internalChannel = req.channel.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.beConservative = beConservative; + flavors.push("_CONSERVATIVE"); + } else { + nonflavors.push("_CONSERVATIVE"); + } + + //GREASE is only used if enabled and not in conservative mode. + if (useGREASE && !beConservative) { + flavors.push("_ECH_GREASE"); + } else { + nonflavors.push("_ECH_GREASE"); + } + + return new Promise((resolve, reject) => { + req.onload = () => { + equal(req.responseText, "OK", "response text should be 'OK'"); + + // Only check telemetry if network process is disabled. + if (!mozinfo.socketprocess_networking) { + HandshakeTelemetryHelpers.checkSuccess(flavors); + HandshakeTelemetryHelpers.checkEmpty(nonflavors); + } + + resolve(); + }; + req.onerror = () => { + ok(false, `should not have gotten an error`); + resolve(); + }; + + req.send(); + }); +} + +function setup() { + Services.prefs.setIntPref("security.tls.version.max", 4); + Services.prefs.setCharPref("network.dns.localDomains", hostname); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); +} +setup(); + +add_task(async function GreaseYConservativeN() { + // First run a server that accepts TLS 1.2 and 1.3. A conservative client + // should succeed in connecting. + let server = startServer(); + + await startClient( + server.port, + true /*be conservative*/, + false /*should succeed*/ + ); + server.close(); +}); + +add_task(async function GreaseNConservativeY() { + // First run a server that accepts TLS 1.2 and 1.3. A conservative client + // should succeed in connecting. + let server = startServer(); + + await startClient( + server.port, + false /*be conservative*/, + true /*should succeed*/ + ); + server.close(); +}); + +add_task(async function GreaseYConservativeY() { + // First run a server that accepts TLS 1.2 and 1.3. A conservative client + // should succeed in connecting. + let server = startServer(); + + await startClient( + server.port, + true /*be conservative*/, + true /*should succeed*/ + ); + server.close(); +}); + +add_task(async function GreaseNConservativeN() { + // First run a server that accepts TLS 1.2 and 1.3. A conservative client + // should succeed in connecting. + let server = startServer(); + + await startClient( + server.port, + false /*be conservative*/, + false /*should succeed*/ + ); + server.close(); +}); + +registerCleanupFunction(function () { + Services.prefs.clearUserPref("security.tls.version.max"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("security.tls.ech.grease_probability"); + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); +}); diff --git a/netwerk/test/unit/test_event_sink.js b/netwerk/test/unit/test_event_sink.js new file mode 100644 index 0000000000..45cbf02f85 --- /dev/null +++ b/netwerk/test/unit/test_event_sink.js @@ -0,0 +1,181 @@ +// This file tests channel event sinks (bug 315598 et al) + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +const sinkCID = Components.ID("{14aa4b81-e266-45cb-88f8-89595dece114}"); +const sinkContract = "@mozilla.org/network/unittest/channeleventsink;1"; + +const categoryName = "net-channel-event-sinks"; + +/** + * This object is both a factory and an nsIChannelEventSink implementation (so, it + * is de-facto a service). It's also an interface requestor that gives out + * itself when asked for nsIChannelEventSink. + */ +var eventsink = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIChannelEventSink"]), + createInstance: function eventsink_ci(iid) { + return this.QueryInterface(iid); + }, + + asyncOnChannelRedirect: function eventsink_onredir( + oldChan, + newChan, + flags, + callback + ) { + // veto + this.called = true; + throw Components.Exception("", Cr.NS_BINDING_ABORTED); + }, + + getInterface: function eventsink_gi(iid) { + if (iid.equals(Ci.nsIChannelEventSink)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + called: false, +}; + +var listener = { + expectSinkCall: true, + + onStartRequest: function test_onStartR(request) { + try { + // Commenting out this check pending resolution of bug 255119 + //if (Components.isSuccessCode(request.status)) + // do_throw("Channel should have a failure code!"); + + // The current URI must be the original URI, as all redirects have been + // cancelled + if ( + !(request instanceof Ci.nsIChannel) || + !request.URI.equals(request.originalURI) + ) { + do_throw( + "Wrong URI: Is <" + + request.URI.spec + + ">, should be <" + + request.originalURI.spec + + ">" + ); + } + + if (request instanceof Ci.nsIHttpChannel) { + // As we expect a blocked redirect, verify that we have a 3xx status + Assert.equal(Math.floor(request.responseStatus / 100), 3); + Assert.equal(request.requestSucceeded, false); + } + + Assert.equal(eventsink.called, this.expectSinkCall); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + if (this._iteration <= 2) { + run_test_continued(); + } else { + do_test_pending(); + httpserv.stop(do_test_finished); + } + do_test_finished(); + }, + + _iteration: 1, +}; + +function makeChan(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var httpserv = null; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/redirect", redirect); + httpserv.registerPathHandler("/redirectfile", redirectfile); + httpserv.start(-1); + + Components.manager.nsIComponentRegistrar.registerFactory( + sinkCID, + "Unit test Event sink", + sinkContract, + eventsink + ); + + // Step 1: Set the callbacks on the listener itself + var chan = makeChan(URL + "/redirect"); + chan.notificationCallbacks = eventsink; + + chan.asyncOpen(listener); + + do_test_pending(); +} + +function run_test_continued() { + eventsink.called = false; + + var chan; + if (listener._iteration == 1) { + // Step 2: Category entry + Services.catMan.addCategoryEntry( + categoryName, + "unit test", + sinkContract, + false, + true + ); + chan = makeChan(URL + "/redirect"); + } else { + // Step 3: Global contract id + Services.catMan.deleteCategoryEntry(categoryName, "unit test", false); + listener.expectSinkCall = false; + chan = makeChan(URL + "/redirectfile"); + } + + listener._iteration++; + chan.asyncOpen(listener); + + do_test_pending(); +} + +// PATHS + +// /redirect +function redirect(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently"); + response.setHeader( + "Location", + "http://localhost:" + metadata.port + "/", + false + ); + + var body = "Moved\n"; + response.bodyOutputStream.write(body, body.length); +} + +// /redirectfile +function redirectfile(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Location", "file:///etc/", false); + + var body = "Attempted to move to a file URI, but failed.\n"; + response.bodyOutputStream.write(body, body.length); +} diff --git a/netwerk/test/unit/test_eviction.js b/netwerk/test/unit/test_eviction.js new file mode 100644 index 0000000000..eac7ece5be --- /dev/null +++ b/netwerk/test/unit/test_eviction.js @@ -0,0 +1,252 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var test_generator = do_run_test(); + +function run_test() { + do_test_pending(); + do_run_generator(test_generator); +} + +function continue_test() { + do_run_generator(test_generator); +} + +function repeat_test() { + // The test is probably going to fail because setting a batch of cookies took + // a significant fraction of 'gPurgeAge'. Compensate by rerunning the + // test with a larger purge age. + Assert.ok(gPurgeAge < 64); + gPurgeAge *= 2; + gShortExpiry *= 2; + + executeSoon(function () { + test_generator.return(); + test_generator = do_run_test(); + do_run_generator(test_generator); + }); +} + +// Purge threshold, in seconds. +var gPurgeAge = 1; + +// Short expiry age, in seconds. +var gShortExpiry = 2; + +// Required delay to ensure a purge occurs, in milliseconds. This must be at +// least gPurgeAge + 10%, and includes a little fuzz to account for timer +// resolution and possible differences between PR_Now() and Date.now(). +function get_purge_delay() { + return gPurgeAge * 1100 + 100; +} + +// Required delay to ensure a cookie set with an expiry time 'gShortExpiry' into +// the future will have expired. +function get_expiry_delay() { + return gShortExpiry * 1000 + 100; +} + +function* do_run_test() { + // Set up a profile. + do_get_profile(); + + // twiddle prefs to convenient values for this test + Services.prefs.setIntPref("network.cookie.purgeAge", gPurgeAge); + Services.prefs.setIntPref("network.cookie.maxNumber", 100); + + let expiry = Date.now() / 1000 + 1000; + + // eviction is performed based on two limits: when the total number of cookies + // exceeds maxNumber + 10% (110), and when cookies are older than purgeAge + // (1 second). purging is done when both conditions are satisfied, and only + // those cookies are purged. + + // we test the following cases of eviction: + // 1) excess and age are satisfied, but only some of the excess are old enough + // to be purged. + Services.cookies.removeAll(); + if (!set_cookies(0, 5, expiry)) { + repeat_test(); + return; + } + // Sleep a while, to make sure the first batch of cookies is older than + // the second (timer resolution varies on different platforms). + do_timeout(get_purge_delay(), continue_test); + yield; + if (!set_cookies(5, 111, expiry)) { + repeat_test(); + return; + } + + // Fake a profile change, to ensure eviction affects the database correctly. + do_close_profile(test_generator); + yield; + do_load_profile(); + Assert.ok(check_remaining_cookies(111, 5, 106)); + + // 2) excess and age are satisfied, and all of the excess are old enough + // to be purged. + Services.cookies.removeAll(); + if (!set_cookies(0, 10, expiry)) { + repeat_test(); + return; + } + do_timeout(get_purge_delay(), continue_test); + yield; + if (!set_cookies(10, 111, expiry)) { + repeat_test(); + return; + } + + do_close_profile(test_generator); + yield; + do_load_profile(); + Assert.ok(check_remaining_cookies(111, 10, 101)); + + // 3) excess and age are satisfied, and more than the excess are old enough + // to be purged. + Services.cookies.removeAll(); + if (!set_cookies(0, 50, expiry)) { + repeat_test(); + return; + } + do_timeout(get_purge_delay(), continue_test); + yield; + if (!set_cookies(50, 111, expiry)) { + repeat_test(); + return; + } + + do_close_profile(test_generator); + yield; + do_load_profile(); + Assert.ok(check_remaining_cookies(111, 50, 101)); + + // 4) excess but not age are satisfied. + Services.cookies.removeAll(); + if (!set_cookies(0, 120, expiry)) { + repeat_test(); + return; + } + + do_close_profile(test_generator); + yield; + do_load_profile(); + Assert.ok(check_remaining_cookies(120, 0, 120)); + + // 5) age but not excess are satisfied. + Services.cookies.removeAll(); + if (!set_cookies(0, 20, expiry)) { + repeat_test(); + return; + } + do_timeout(get_purge_delay(), continue_test); + yield; + if (!set_cookies(20, 110, expiry)) { + repeat_test(); + return; + } + + do_close_profile(test_generator); + yield; + do_load_profile(); + Assert.ok(check_remaining_cookies(110, 20, 110)); + + // 6) Excess and age are satisfied, but the cookie limit can be satisfied by + // purging expired cookies. + Services.cookies.removeAll(); + let shortExpiry = Math.floor(Date.now() / 1000) + gShortExpiry; + if (!set_cookies(0, 20, shortExpiry)) { + repeat_test(); + return; + } + do_timeout(get_expiry_delay(), continue_test); + yield; + if (!set_cookies(20, 110, expiry)) { + repeat_test(); + return; + } + do_timeout(get_purge_delay(), continue_test); + yield; + if (!set_cookies(110, 111, expiry)) { + repeat_test(); + return; + } + + do_close_profile(test_generator); + yield; + do_load_profile(); + Assert.ok(check_remaining_cookies(111, 20, 91)); + + do_finish_generator_test(test_generator); +} + +// Set 'end - begin' total cookies, with consecutively increasing hosts numbered +// 'begin' to 'end'. +function set_cookies(begin, end, expiry) { + Assert.ok(begin != end); + + let beginTime; + for (let i = begin; i < end; ++i) { + let host = "eviction." + i + ".tests"; + Services.cookies.add( + host, + "", + "test", + "eviction", + false, + false, + false, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + + if (i == begin) { + beginTime = get_creationTime(i); + } + } + + let endTime = get_creationTime(end - 1); + Assert.ok(begin == end - 1 || endTime > beginTime); + if (endTime - beginTime > gPurgeAge * 1000000) { + // Setting cookies took an amount of time very close to the purge threshold. + // Retry the test with a larger threshold. + return false; + } + + return true; +} + +function get_creationTime(i) { + let host = "eviction." + i + ".tests"; + let cookies = Services.cookies.getCookiesFromHost(host, {}); + Assert.ok(cookies.length); + let cookie = cookies[0]; + return cookie.creationTime; +} + +// Test that 'aNumberToExpect' cookies remain after purging is complete, and +// that the cookies that remain consist of the set expected given the number of +// of older and newer cookies -- eviction should occur by order of lastAccessed +// time, if both the limit on total cookies (maxNumber + 10%) and the purge age +// + 10% are exceeded. +function check_remaining_cookies(aNumberTotal, aNumberOld, aNumberToExpect) { + let i = 0; + for (let cookie of Services.cookies.cookies) { + ++i; + + if (aNumberTotal != aNumberToExpect) { + // make sure the cookie is one of the batch we expect was purged. + var hostNumber = Number(cookie.rawHost.split(".")[1]); + if (hostNumber < aNumberOld - aNumberToExpect) { + break; + } + } + } + + return i == aNumberToExpect; +} diff --git a/netwerk/test/unit/test_extract_charset_from_content_type.js b/netwerk/test/unit/test_extract_charset_from_content_type.js new file mode 100644 index 0000000000..a90e646048 --- /dev/null +++ b/netwerk/test/unit/test_extract_charset_from_content_type.js @@ -0,0 +1,238 @@ +/* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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"; + +var charset = {}; +var charsetStart = {}; +var charsetEnd = {}; +var hadCharset; + +function check(aHadCharset, aCharset, aCharsetStart, aCharsetEnd) { + Assert.equal(aHadCharset, hadCharset); + Assert.equal(aCharset, charset.value); + Assert.equal(aCharsetStart, charsetStart.value); + Assert.equal(aCharsetEnd, charsetEnd.value); +} + +function run_test() { + var netutil = Services.io; + hadCharset = netutil.extractCharsetFromContentType( + "text/html", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 9, 9); + + hadCharset = netutil.extractCharsetFromContentType( + "TEXT/HTML", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 9, 9); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html, text/html", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 9, 9); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html, text/plain", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 21, 21); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html, ", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 9, 9); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html, */*", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 9, 9); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html, foo", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 9, 9); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html; charset=ISO-8859-1", + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 9, 29); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html ; charset=ISO-8859-1", + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 11, 34); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html ; charset=ISO-8859-1 ", + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 11, 36); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html ; charset=ISO-8859-1 ; ", + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 11, 35); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/html; charset="ISO-8859-1"', + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 9, 31); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html; charset='ISO-8859-1'", + charset, + charsetStart, + charsetEnd + ); + check(true, "'ISO-8859-1'", 9, 31); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/html; charset="ISO-8859-1", text/html', + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 9, 31); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/html; charset="ISO-8859-1", text/html; charset=UTF8', + charset, + charsetStart, + charsetEnd + ); + check(true, "UTF8", 42, 56); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html; charset=ISO-8859-1, TEXT/HTML", + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 9, 29); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html; charset=ISO-8859-1, TEXT/plain", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 41, 41); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain, TEXT/HTML; charset="ISO-8859-1", text/html, TEXT/HTML', + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 21, 43); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain, TEXT/HTML; param="charset=UTF8"; charset="ISO-8859-1"; param2="charset=UTF16", text/html, TEXT/HTML', + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 43, 65); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain, TEXT/HTML; param=charset=UTF8; charset="ISO-8859-1"; param2=charset=UTF16, text/html, TEXT/HTML', + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 41, 63); + + hadCharset = netutil.extractCharsetFromContentType( + "text/plain; param= , text/html", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 30, 30); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain; param=", text/html"', + charset, + charsetStart, + charsetEnd + ); + check(false, "", 10, 10); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain; param=", \\" , text/html"', + charset, + charsetStart, + charsetEnd + ); + check(false, "", 10, 10); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain; param=", \\" , text/html , "', + charset, + charsetStart, + charsetEnd + ); + check(false, "", 10, 10); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain param=", \\" , text/html , "', + charset, + charsetStart, + charsetEnd + ); + check(false, "", 38, 38); + + hadCharset = netutil.extractCharsetFromContentType( + "text/plain charset=UTF8", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 23, 23); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain, TEXT/HTML; param="charset=UTF8"; ; param2="charset=UTF16", text/html, TEXT/HTML', + charset, + charsetStart, + charsetEnd + ); + check(false, "", 21, 21); +} diff --git a/netwerk/test/unit/test_file_protocol.js b/netwerk/test/unit/test_file_protocol.js new file mode 100644 index 0000000000..707bddef24 --- /dev/null +++ b/netwerk/test/unit/test_file_protocol.js @@ -0,0 +1,277 @@ +/* run some tests on the file:// protocol handler */ + +"use strict"; + +const PR_RDONLY = 0x1; // see prio.h + +const special_type = "application/x-our-special-type"; + +[ + test_read_file, + test_read_dir_1, + test_read_dir_2, + test_upload_file, + test_load_shelllink, + do_test_finished, +].forEach(f => add_test(f)); + +function new_file_input_stream(file, buffered) { + var stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + stream.init(file, PR_RDONLY, 0, 0); + if (!buffered) { + return stream; + } + + var buffer = Cc[ + "@mozilla.org/network/buffered-input-stream;1" + ].createInstance(Ci.nsIBufferedInputStream); + buffer.init(stream, 4096); + return buffer; +} + +function new_file_channel(file) { + let uri = Services.io.newFileURI(file); + return NetUtil.newChannel({ + uri, + loadingPrincipal: Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ), + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); +} + +/* + * stream listener + * this listener has some additional file-specific tests, so we can't just use + * ChannelListener here. + */ +function FileStreamListener(closure) { + this._closure = closure; +} +FileStreamListener.prototype = { + _closure: null, + _buffer: "", + _got_onstartrequest: false, + _got_onstoprequest: false, + _contentLen: -1, + + _isDir(request) { + request.QueryInterface(Ci.nsIFileChannel); + return request.file.isDirectory(); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + if (this._got_onstartrequest) { + do_throw("Got second onStartRequest event!"); + } + this._got_onstartrequest = true; + + if (!this._isDir(request)) { + request.QueryInterface(Ci.nsIChannel); + this._contentLen = request.contentLength; + if (this._contentLen == -1) { + do_throw("Content length is unknown in onStartRequest!"); + } + } + }, + + onDataAvailable(request, stream, offset, count) { + if (!this._got_onstartrequest) { + do_throw("onDataAvailable without onStartRequest event!"); + } + if (this._got_onstoprequest) { + do_throw("onDataAvailable after onStopRequest event!"); + } + if (!request.isPending()) { + do_throw("request reports itself as not pending from onStartRequest!"); + } + + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + + onStopRequest(request, status) { + if (!this._got_onstartrequest) { + do_throw("onStopRequest without onStartRequest event!"); + } + if (this._got_onstoprequest) { + do_throw("Got second onStopRequest event!"); + } + this._got_onstoprequest = true; + if (!Components.isSuccessCode(status)) { + do_throw("Failed to load file: " + status.toString(16)); + } + if (status != request.status) { + do_throw("request.status does not match status arg to onStopRequest!"); + } + if (request.isPending()) { + do_throw("request reports itself as pending from onStopRequest!"); + } + if (this._contentLen != -1 && this._buffer.length != this._contentLen) { + do_throw("did not read nsIChannel.contentLength number of bytes!"); + } + + this._closure(this._buffer); + }, +}; + +function test_read_file() { + dump("*** test_read_file\n"); + + var file = do_get_file("../unit/data/test_readline6.txt"); + var chan = new_file_channel(file); + + function on_read_complete(data) { + dump("*** test_read_file.on_read_complete\n"); + + // bug 326693 + if (chan.contentType != special_type) { + do_throw( + "Type mismatch! Is <" + + chan.contentType + + ">, should be <" + + special_type + + ">" + ); + } + + /* read completed successfully. now read data directly from file, + and compare the result. */ + var stream = new_file_input_stream(file, false); + var result = read_stream(stream, stream.available()); + if (result != data) { + do_throw("Stream contents do not match with direct read!"); + } + run_next_test(); + } + + chan.contentType = special_type; + chan.asyncOpen(new FileStreamListener(on_read_complete)); +} + +function do_test_read_dir(set_type, expected_type) { + dump("*** test_read_dir(" + set_type + ", " + expected_type + ")\n"); + + var file = do_get_tempdir(); + var chan = new_file_channel(file); + + function on_read_complete(data) { + dump( + "*** test_read_dir.on_read_complete(" + + set_type + + ", " + + expected_type + + ")\n" + ); + + // bug 326693 + if (chan.contentType != expected_type) { + do_throw( + "Type mismatch! Is <" + + chan.contentType + + ">, should be <" + + expected_type + + ">" + ); + } + + run_next_test(); + } + + if (set_type) { + chan.contentType = expected_type; + } + chan.asyncOpen(new FileStreamListener(on_read_complete)); +} + +function test_read_dir_1() { + return do_test_read_dir(false, "application/http-index-format"); +} + +function test_read_dir_2() { + return do_test_read_dir(true, special_type); +} + +function test_upload_file() { + dump("*** test_upload_file\n"); + + var file = do_get_file("../unit/data/test_readline6.txt"); // file to upload + var dest = do_get_tempdir(); // file upload destination + dest.append("junk.dat"); + dest.createUnique(dest.NORMAL_FILE_TYPE, 0o600); + + var uploadstream = new_file_input_stream(file, true); + + var chan = new_file_channel(dest); + chan.QueryInterface(Ci.nsIUploadChannel); + chan.setUploadStream(uploadstream, "", file.fileSize); + + function on_upload_complete(data) { + dump("*** test_upload_file.on_upload_complete\n"); + + // bug 326693 + if (chan.contentType != special_type) { + do_throw( + "Type mismatch! Is <" + + chan.contentType + + ">, should be <" + + special_type + + ">" + ); + } + + /* upload of file completed successfully. */ + if (data.length) { + do_throw("Upload resulted in data!"); + } + + var oldstream = new_file_input_stream(file, false); + var newstream = new_file_input_stream(dest, false); + var olddata = read_stream(oldstream, oldstream.available()); + var newdata = read_stream(newstream, newstream.available()); + if (olddata != newdata) { + do_throw("Stream contents do not match after file copy!"); + } + oldstream.close(); + newstream.close(); + + /* cleanup... also ensures that the destination file is not in + use when OnStopRequest is called. */ + try { + dest.remove(false); + } catch (e) { + dump(e + "\n"); + do_throw("Unable to remove uploaded file!\n"); + } + + run_next_test(); + } + + chan.contentType = special_type; + chan.asyncOpen(new FileStreamListener(on_upload_complete)); +} + +function test_load_shelllink() { + // lnk files should not resolve to their targets + dump("*** test_load_shelllink\n"); + let file = do_get_file("data/system_root.lnk", false); + var chan = new_file_channel(file); + + // The original URI path should be the same as the URI path + Assert.equal(chan.URI.pathQueryRef, chan.originalURI.pathQueryRef); + + // The original URI path should be the same as the lnk file path + Assert.equal( + chan.originalURI.pathQueryRef, + Services.io.newFileURI(file).pathQueryRef + ); + run_next_test(); +} diff --git a/netwerk/test/unit/test_filestreams.js b/netwerk/test/unit/test_filestreams.js new file mode 100644 index 0000000000..b1edf3d27b --- /dev/null +++ b/netwerk/test/unit/test_filestreams.js @@ -0,0 +1,300 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// We need the profile directory so the test harness will clean up our test +// files. +do_get_profile(); + +const OUTPUT_STREAM_CONTRACT_ID = "@mozilla.org/network/file-output-stream;1"; +const SAFE_OUTPUT_STREAM_CONTRACT_ID = + "@mozilla.org/network/safe-file-output-stream;1"; + +//////////////////////////////////////////////////////////////////////////////// +//// Helper Methods + +/** + * Generates a leafName for a file that does not exist, but does *not* + * create the file. Similar to createUnique except for the fact that createUnique + * does create the file. + * + * @param aFile + * The file to modify in order for it to have a unique leafname. + */ +function ensure_unique(aFile) { + ensure_unique.fileIndex = ensure_unique.fileIndex || 0; + + var leafName = aFile.leafName; + while (aFile.clone().exists()) { + aFile.leafName = leafName + "_" + ensure_unique.fileIndex++; + } +} + +/** + * Tests for files being accessed at the right time. Streams that use + * DEFER_OPEN should only open or create the file when an operation is + * done, and not during Init(). + * + * Note that for writing, we check for actual writing in test_NetUtil (async) + * and in sync_operations in this file (sync), whereas in this function we + * just check that the file is *not* created during init. + * + * @param aContractId + * The contract ID to use for the output stream + * @param aDeferOpen + * Whether to check with DEFER_OPEN or not + * @param aTrickDeferredOpen + * Whether we try to 'trick' deferred opens by changing the file object before + * the actual open. The stream should have a clone, so changes to the file + * object after Init and before Open should not affect it. + */ +function check_access(aContractId, aDeferOpen, aTrickDeferredOpen) { + const LEAF_NAME = "filestreams-test-file.tmp"; + const TRICKY_LEAF_NAME = "BetYouDidNotExpectThat.tmp"; + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(LEAF_NAME); + + // Writing + + ensure_unique(file); + let ostream = Cc[aContractId].createInstance(Ci.nsIFileOutputStream); + ostream.init( + file, + -1, + -1, + aDeferOpen ? Ci.nsIFileOutputStream.DEFER_OPEN : 0 + ); + Assert.equal(aDeferOpen, !file.clone().exists()); // If defer, should not exist and vice versa + if (aDeferOpen) { + // File should appear when we do write to it. + if (aTrickDeferredOpen) { + // See |@param aDeferOpen| in the JavaDoc comment for this function + file.leafName = TRICKY_LEAF_NAME; + } + ostream.write("data", 4); + if (aTrickDeferredOpen) { + file.leafName = LEAF_NAME; + } + // We did a write, so the file should now exist + Assert.ok(file.clone().exists()); + } + ostream.close(); + + // Reading + + ensure_unique(file); + let istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + var initOk, getOk; + try { + istream.init( + file, + -1, + 0, + aDeferOpen ? Ci.nsIFileInputStream.DEFER_OPEN : 0 + ); + initOk = true; + } catch (e) { + initOk = false; + } + try { + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(file, -1, 0, 0); + getOk = true; + } catch (e) { + getOk = false; + } + + // If the open is deferred, then Init should succeed even though the file we + // intend to read does not exist, and then trying to read from it should + // fail. The other case is where the open is not deferred, and there we should + // get an error when we Init (and also when we try to read). + Assert.ok( + (aDeferOpen && initOk && !getOk) || (!aDeferOpen && !initOk && !getOk) + ); + istream.close(); +} + +/** + * We test async operations in test_NetUtil.js, and here test for simple sync + * operations on input streams. + * + * @param aDeferOpen + * Whether to use DEFER_OPEN in the streams. + */ +function sync_operations(aDeferOpen) { + const TEST_DATA = "this is a test string"; + const LEAF_NAME = "filestreams-test-file.tmp"; + + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(LEAF_NAME); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + let ostream = Cc[OUTPUT_STREAM_CONTRACT_ID].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init( + file, + -1, + -1, + aDeferOpen ? Ci.nsIFileOutputStream.DEFER_OPEN : 0 + ); + + ostream.write(TEST_DATA, TEST_DATA.length); + ostream.close(); + + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(file, -1, 0, aDeferOpen ? Ci.nsIFileInputStream.DEFER_OPEN : 0); + + let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance( + Ci.nsIConverterInputStream + ); + cstream.init(fstream, "UTF-8", 0, 0); + + let string = {}; + cstream.readString(-1, string); + cstream.close(); + fstream.close(); + + Assert.equal(string.value, TEST_DATA); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +function test_access() { + check_access(OUTPUT_STREAM_CONTRACT_ID, false, false); +} + +function test_access_trick() { + check_access(OUTPUT_STREAM_CONTRACT_ID, false, true); +} + +function test_access_defer() { + check_access(OUTPUT_STREAM_CONTRACT_ID, true, false); +} + +function test_access_defer_trick() { + check_access(OUTPUT_STREAM_CONTRACT_ID, true, true); +} + +function test_access_safe() { + check_access(SAFE_OUTPUT_STREAM_CONTRACT_ID, false, false); +} + +function test_access_safe_trick() { + check_access(SAFE_OUTPUT_STREAM_CONTRACT_ID, false, true); +} + +function test_access_safe_defer() { + check_access(SAFE_OUTPUT_STREAM_CONTRACT_ID, true, false); +} + +function test_access_safe_defer_trick() { + check_access(SAFE_OUTPUT_STREAM_CONTRACT_ID, true, true); +} + +function test_sync_operations() { + sync_operations(); +} + +function test_sync_operations_deferred() { + sync_operations(true); +} + +function do_test_zero_size_buffered(disableBuffering) { + const LEAF_NAME = "filestreams-test-file.tmp"; + const BUFFERSIZE = 4096; + + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(LEAF_NAME); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init( + file, + -1, + 0, + Ci.nsIFileInputStream.CLOSE_ON_EOF | Ci.nsIFileInputStream.REOPEN_ON_REWIND + ); + + var buffered = Cc[ + "@mozilla.org/network/buffered-input-stream;1" + ].createInstance(Ci.nsIBufferedInputStream); + buffered.init(fstream, BUFFERSIZE); + + if (disableBuffering) { + buffered.QueryInterface(Ci.nsIStreamBufferAccess).disableBuffering(); + } + + // Scriptable input streams clamp read sizes to the return value of + // available(), so don't quite do what we want here. + let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance( + Ci.nsIConverterInputStream + ); + cstream.init(buffered, "UTF-8", 0, 0); + + Assert.equal(buffered.available(), 0); + + // Now try reading from this stream + let string = {}; + Assert.equal(cstream.readString(BUFFERSIZE, string), 0); + Assert.equal(string.value, ""); + + // Now check that available() throws + var exceptionThrown = false; + try { + Assert.equal(buffered.available(), 0); + } catch (e) { + exceptionThrown = true; + } + Assert.ok(exceptionThrown); + + // OK, now seek back to start + buffered.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + + // Now check that available() does not throw + exceptionThrown = false; + try { + Assert.equal(buffered.available(), 0); + } catch (e) { + exceptionThrown = true; + } + Assert.ok(!exceptionThrown); +} + +function test_zero_size_buffered() { + do_test_zero_size_buffered(false); + do_test_zero_size_buffered(true); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Test Runner + +var tests = [ + test_access, + test_access_trick, + test_access_defer, + test_access_defer_trick, + test_access_safe, + test_access_safe_trick, + test_access_safe_defer, + test_access_safe_defer_trick, + test_sync_operations, + test_sync_operations_deferred, + test_zero_size_buffered, +]; + +function run_test() { + tests.forEach(function (test) { + test(); + }); +} diff --git a/netwerk/test/unit/test_freshconnection.js b/netwerk/test/unit/test_freshconnection.js new file mode 100644 index 0000000000..5d0f5bc5b7 --- /dev/null +++ b/netwerk/test/unit/test_freshconnection.js @@ -0,0 +1,30 @@ +// This is essentially a debug mode crashtest to make sure everything +// involved in a reload runs on the right thread. It relies on the +// assertions in necko. + +"use strict"; + +var listener = { + onStartRequest: function test_onStartR(request) {}, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + do_test_finished(); + }, +}; + +function run_test() { + var chan = NetUtil.newChannel({ + uri: "http://localhost:4444", + loadUsingSystemPrincipal: true, + }); + chan.loadFlags = + Ci.nsIRequest.LOAD_FRESH_CONNECTION | + Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + chan.QueryInterface(Ci.nsIHttpChannel); + chan.asyncOpen(listener); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_getHost.js b/netwerk/test/unit/test_getHost.js new file mode 100644 index 0000000000..90db800575 --- /dev/null +++ b/netwerk/test/unit/test_getHost.js @@ -0,0 +1,63 @@ +// Test getLocalHost/getLocalPort and getRemoteHost/getRemotePort. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +httpserver.start(-1); +const PORT = httpserver.identity.primaryPort; + +var gotOnStartRequest = false; + +function CheckGetHostListener() {} + +CheckGetHostListener.prototype = { + onStartRequest(request) { + dump("*** listener onStartRequest\n"); + + gotOnStartRequest = true; + + request.QueryInterface(Ci.nsIHttpChannelInternal); + try { + Assert.equal(request.localAddress, "127.0.0.1"); + Assert.equal(request.localPort > 0, true); + Assert.notEqual(request.localPort, PORT); + Assert.equal(request.remoteAddress, "127.0.0.1"); + Assert.equal(request.remotePort, PORT); + } catch (e) { + Assert.ok(0, "Get local/remote host/port throws an error!"); + } + }, + + onStopRequest(request, statusCode) { + dump("*** listener onStopRequest\n"); + + Assert.equal(gotOnStartRequest, true); + httpserver.stop(do_test_finished); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]), +}; + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function test_handler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + var responseBody = "blah blah"; + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + httpserver.registerPathHandler("/testdir", test_handler); + + var channel = make_channel("http://localhost:" + PORT + "/testdir"); + channel.asyncOpen(new CheckGetHostListener()); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_gio_protocol.js b/netwerk/test/unit/test_gio_protocol.js new file mode 100644 index 0000000000..37ce37abab --- /dev/null +++ b/netwerk/test/unit/test_gio_protocol.js @@ -0,0 +1,201 @@ +/* run some tests on the gvfs/gio protocol handler */ + +"use strict"; + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +const PR_RDONLY = 0x1; // see prio.h + +[ + do_test_read_data_dir, + do_test_read_recent, + test_read_file, + do_test_finished, +].forEach(f => add_test(f)); + +function setup() { + // Allowing some protocols to get a channel + if (!inChildProcess()) { + Services.prefs.setCharPref( + "network.gio.supported-protocols", + "localtest:,recent:" + ); + } else { + do_send_remote_message("gio-allow-test-protocols"); + do_await_remote_message("gio-allow-test-protocols-done"); + } +} + +setup(); + +registerCleanupFunction(() => { + // Resetting the protocols to None + if (!inChildProcess()) { + Services.prefs.clearUserPref("network.gio.supported-protocols"); + } +}); + +function new_file_input_stream(file, buffered) { + var stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + stream.init(file, PR_RDONLY, 0, 0); + if (!buffered) { + return stream; + } + + var buffer = Cc[ + "@mozilla.org/network/buffered-input-stream;1" + ].createInstance(Ci.nsIBufferedInputStream); + buffer.init(stream, 4096); + return buffer; +} + +function new_file_channel(file) { + var chan = NetUtil.newChannel({ + uri: file, + loadUsingSystemPrincipal: true, + }); + + return chan; +} + +/* + * stream listener + * this listener has some additional file-specific tests, so we can't just use + * ChannelListener here. + */ +function FileStreamListener(closure) { + this._closure = closure; +} +FileStreamListener.prototype = { + _closure: null, + _buffer: "", + _got_onstartrequest: false, + _got_onstoprequest: false, + _contentLen: -1, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + if (this._got_onstartrequest) { + do_throw("Got second onStartRequest event!"); + } + this._got_onstartrequest = true; + }, + + onDataAvailable(request, stream, offset, count) { + if (!this._got_onstartrequest) { + do_throw("onDataAvailable without onStartRequest event!"); + } + if (this._got_onstoprequest) { + do_throw("onDataAvailable after onStopRequest event!"); + } + if (!request.isPending()) { + do_throw("request reports itself as not pending from onStartRequest!"); + } + + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + + onStopRequest(request, status) { + if (!this._got_onstartrequest) { + do_throw("onStopRequest without onStartRequest event!"); + } + if (this._got_onstoprequest) { + do_throw("Got second onStopRequest event!"); + } + this._got_onstoprequest = true; + if (!Components.isSuccessCode(status)) { + do_throw("Failed to load file: " + status.toString(16)); + } + if (status != request.status) { + do_throw("request.status does not match status arg to onStopRequest!"); + } + if (request.isPending()) { + do_throw("request reports itself as pending from onStopRequest!"); + } + if (this._contentLen != -1 && this._buffer.length != this._contentLen) { + do_throw("did not read nsIChannel.contentLength number of bytes!"); + } + + this._closure(this._buffer); + }, +}; + +function test_read_file() { + dump("*** test_read_file\n"); + // Going via parent path, because this is opended from test/unit/ and test/unit_ipc/ + var file = do_get_file("../unit/data/test_readline4.txt"); + var chan = new_file_channel("localtest://" + file.path); + + function on_read_complete(data) { + dump("*** test_read_file.on_read_complete()\n"); + /* read completed successfully. now read data directly from file, + and compare the result. */ + var stream = new_file_input_stream(file, false); + var result = read_stream(stream, stream.available()); + if (result != data) { + do_throw("Stream contents do not match with direct read!"); + } + run_next_test(); + } + + chan.asyncOpen(new FileStreamListener(on_read_complete)); +} + +function do_test_read_data_dir() { + dump('*** test_read_data_dir("../data/")\n'); + + var dir = do_get_file("../unit/data/"); + var chan = new_file_channel("localtest://" + dir.path); + + function on_read_complete(data) { + dump("*** test_read_data_dir.on_read_complete()\n"); + + // The data-directory should be listed, containing a header-line and the files therein + if ( + !( + data.includes("200: filename content-length last-modified file-type") && + data.includes("201: test_readline1.txt") && + data.includes("201: test_readline2.txt") + ) + ) { + do_throw( + "test_read_data_dir() - Bad data! Does not contain needles! Is <" + + data + + ">" + ); + } + run_next_test(); + } + chan.asyncOpen(new FileStreamListener(on_read_complete)); +} + +function do_test_read_recent() { + dump('*** test_read_recent("recent://")\n'); + + var chan = new_file_channel("recent:///"); + + function on_read_complete(data) { + dump("*** test_read_recent.on_read_complete()\n"); + + // The data-directory should be listed, containing a header-line and the files therein + if ( + !data.includes("200: filename content-length last-modified file-type") + ) { + do_throw( + "do_test_read_recent() - Bad data! Does not contain header! Is <" + + data + + ">" + ); + } + run_next_test(); + } + chan.asyncOpen(new FileStreamListener(on_read_complete)); +} diff --git a/netwerk/test/unit/test_gre_resources.js b/netwerk/test/unit/test_gre_resources.js new file mode 100644 index 0000000000..4ea8d04b95 --- /dev/null +++ b/netwerk/test/unit/test_gre_resources.js @@ -0,0 +1,30 @@ +// test that things that are expected to be in gre-resources are still there + +"use strict"; + +function wrapInputStream(input) { + var nsIScriptableInputStream = Ci.nsIScriptableInputStream; + var factory = Cc["@mozilla.org/scriptableinputstream;1"]; + var wrapper = factory.createInstance(nsIScriptableInputStream); + wrapper.init(input); + return wrapper; +} + +function check_file(file) { + var channel = NetUtil.newChannel({ + uri: "resource://gre-resources/" + file, + loadUsingSystemPrincipal: true, + }); + try { + let instr = wrapInputStream(channel.open()); + Assert.ok(!!instr.read(1024).length); + } catch (e) { + do_throw("Failed to read " + file + " from gre-resources:" + e); + } +} + +function run_test() { + for (let file of ["ua.css"]) { + check_file(file); + } +} diff --git a/netwerk/test/unit/test_gzipped_206.js b/netwerk/test/unit/test_gzipped_206.js new file mode 100644 index 0000000000..cd00a109a2 --- /dev/null +++ b/netwerk/test/unit/test_gzipped_206.js @@ -0,0 +1,118 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; + +// testString = "This is a slightly longer test\n"; +const responseBody = [ + 0x1f, 0x8b, 0x08, 0x08, 0xef, 0x70, 0xe6, 0x4c, 0x00, 0x03, 0x74, 0x65, 0x78, + 0x74, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x74, 0x78, 0x74, 0x00, 0x0b, 0xc9, 0xc8, + 0x2c, 0x56, 0x00, 0xa2, 0x44, 0x85, 0xe2, 0x9c, 0xcc, 0xf4, 0x8c, 0x92, 0x9c, + 0x4a, 0x85, 0x9c, 0xfc, 0xbc, 0xf4, 0xd4, 0x22, 0x85, 0x92, 0xd4, 0xe2, 0x12, + 0x2e, 0x2e, 0x00, 0x00, 0xe5, 0xe6, 0xf0, 0x20, 0x00, 0x00, 0x00, +]; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var doRangeResponse = false; + +function cachedHandler(metadata, response) { + response.setHeader("Content-Type", "application/x-gzip", false); + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=3600000"); // avoid validation + + var body = responseBody; + + if (doRangeResponse) { + Assert.ok(metadata.hasHeader("Range")); + var matches = metadata + .getHeader("Range") + .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/); + var from = matches[1] === undefined ? 0 : matches[1]; + var to = matches[2] === undefined ? responseBody.length - 1 : matches[2]; + if (from >= responseBody.length) { + response.setStatusLine(metadata.httpVersion, 416, "Start pos too high"); + response.setHeader("Content-Range", "*/" + responseBody.length, false); + return; + } + body = body.slice(from, to + 1); + response.setHeader("Content-Length", "" + (to + 1 - from)); + // always respond to successful range requests with 206 + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader( + "Content-Range", + from + "-" + to + "/" + responseBody.length, + false + ); + } else { + // This response will get cut off prematurely + response.setHeader("Content-Length", "" + responseBody.length); + response.setHeader("Accept-Ranges", "bytes"); + body = body.slice(0, 17); // slice off a piece to send first + doRangeResponse = true; + } + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + bos.setOutputStream(response.bodyOutputStream); + + response.processAsync(); + bos.writeByteArray(body); + response.finish(); +} + +function continue_test(request, data) { + Assert.equal(17, data.length); + var chan = make_channel( + "http://localhost:" + httpserver.identity.primaryPort + "/cached/test.gz" + ); + chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_GZIP)); +} + +var enforcePref; +var clearBogusContentEncodingPref; + +function finish_test(request, data, ctx) { + Assert.equal(request.status, 0); + Assert.equal(data.length, responseBody.length); + for (var i = 0; i < data.length; ++i) { + Assert.equal(data.charCodeAt(i), responseBody[i]); + } + Services.prefs.setBoolPref("network.http.enforce-framing.http1", enforcePref); + Services.prefs.setBoolPref( + "network.http.clear_bogus_content_encoding", + clearBogusContentEncodingPref + ); + httpserver.stop(do_test_finished); +} + +function run_test() { + clearBogusContentEncodingPref = Services.prefs.getBoolPref( + "network.http.clear_bogus_content_encoding" + ); + Services.prefs.setBoolPref("network.http.clear_bogus_content_encoding", true); + enforcePref = Services.prefs.getBoolPref( + "network.http.enforce-framing.http1" + ); + Services.prefs.setBoolPref("network.http.enforce-framing.http1", false); + + httpserver = new HttpServer(); + httpserver.registerPathHandler("/cached/test.gz", cachedHandler); + httpserver.start(-1); + + // wipe out cached content + evict_cache_entries(); + + var chan = make_channel( + "http://localhost:" + httpserver.identity.primaryPort + "/cached/test.gz" + ); + chan.asyncOpen( + new ChannelListener(continue_test, null, CL_EXPECT_GZIP | CL_IGNORE_CL) + ); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_h2proxy_connection_limit.js b/netwerk/test/unit/test_h2proxy_connection_limit.js new file mode 100644 index 0000000000..f326b48b40 --- /dev/null +++ b/netwerk/test/unit/test_h2proxy_connection_limit.js @@ -0,0 +1,77 @@ +/* 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/. */ + +// Summary: +// Test whether the connection limit is honored when http2 proxy is used. +// +// Test step: +// 1. Create 30 http requests. +// 2. Check if the count of all sockets created by proxy is less than 6. + +"use strict"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +add_task(async function test_connection_limit() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); + registerCleanupFunction(async () => { + await proxy.stop(); + }); + + const maxConnections = 6; + Services.prefs.setIntPref( + "network.http.max-persistent-connections-per-server", + maxConnections + ); + registerCleanupFunction(async () => { + Services.prefs.clearUserPref( + "network.http.max-persistent-connections-per-server" + ); + }); + + await with_node_servers([NodeHTTP2Server], async server => { + await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200); + resp.end("All good"); + }); + + let promises = []; + for (let i = 0; i < 30; ++i) { + let chan = makeChan(`${server.origin()}/test`); + promises.push( + new Promise(resolve => { + chan.asyncOpen( + new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL) + ); + }) + ); + } + await Promise.all(promises); + let count = await proxy.socketCount(server.port()); + Assert.lessOrEqual( + count, + maxConnections, + "socket count should be less than maxConnections" + ); + }); +}); diff --git a/netwerk/test/unit/test_head.js b/netwerk/test/unit/test_head.js new file mode 100644 index 0000000000..264c9fbe13 --- /dev/null +++ b/netwerk/test/unit/test_head.js @@ -0,0 +1,169 @@ +// +// HTTP headers test +// + +// Note: sets Cc and Ci variables + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var testpath = "/simple"; +var httpbody = "0123456789"; +var channel; + +var dbg = 0; +if (dbg) { + print("============== START =========="); +} + +function run_test() { + setup_test(); + do_test_pending(); +} + +function setup_test() { + if (dbg) { + print("============== setup_test: in"); + } + + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + + channel = setupChannel(testpath); + + channel.setRequestHeader("ReplaceMe", "initial value", true); + var setOK = channel.getRequestHeader("ReplaceMe"); + Assert.equal(setOK, "initial value"); + channel.setRequestHeader("ReplaceMe", "replaced", false); + setOK = channel.getRequestHeader("ReplaceMe"); + Assert.equal(setOK, "replaced"); + + channel.setRequestHeader("MergeMe", "foo1", true); + channel.setRequestHeader("MergeMe", "foo2", true); + channel.setRequestHeader("MergeMe", "foo3", true); + setOK = channel.getRequestHeader("MergeMe"); + Assert.equal(setOK, "foo1, foo2, foo3"); + + channel.setEmptyRequestHeader("Empty"); + setOK = channel.getRequestHeader("Empty"); + Assert.equal(setOK, ""); + + channel.setRequestHeader("ReplaceWithEmpty", "initial value", true); + setOK = channel.getRequestHeader("ReplaceWithEmpty"); + Assert.equal(setOK, "initial value"); + channel.setEmptyRequestHeader("ReplaceWithEmpty"); + setOK = channel.getRequestHeader("ReplaceWithEmpty"); + Assert.equal(setOK, ""); + + channel.setEmptyRequestHeader("MergeWithEmpty"); + setOK = channel.getRequestHeader("MergeWithEmpty"); + Assert.equal(setOK, ""); + channel.setRequestHeader("MergeWithEmpty", "foo", true); + setOK = channel.getRequestHeader("MergeWithEmpty"); + Assert.equal(setOK, "foo"); + + var uri = NetUtil.newURI("http://foo1.invalid:80"); + channel.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri); + setOK = channel.getRequestHeader("Referer"); + Assert.equal(setOK, "http://foo1.invalid/"); + + uri = NetUtil.newURI("http://foo2.invalid:90/bar"); + channel.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri); + setOK = channel.getRequestHeader("Referer"); + // No triggering URI inloadInfo, assume load is cross-origin. + Assert.equal(setOK, "http://foo2.invalid:90/"); + + // ChannelListener defined in head_channels.js + channel.asyncOpen(new ChannelListener(checkRequestResponse, channel)); + + if (dbg) { + print("============== setup_test: out"); + } +} + +function setupChannel(path) { + var chan = NetUtil.newChannel({ + uri: URL + path, + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + return chan; +} + +function serverHandler(metadata, response) { + if (dbg) { + print("============== serverHandler: in"); + } + + var setOK = metadata.getHeader("ReplaceMe"); + Assert.equal(setOK, "replaced"); + setOK = metadata.getHeader("MergeMe"); + Assert.equal(setOK, "foo1, foo2, foo3"); + setOK = metadata.getHeader("Empty"); + Assert.equal(setOK, ""); + setOK = metadata.getHeader("ReplaceWithEmpty"); + Assert.equal(setOK, ""); + setOK = metadata.getHeader("MergeWithEmpty"); + Assert.equal(setOK, "foo"); + setOK = metadata.getHeader("Referer"); + Assert.equal(setOK, "http://foo2.invalid:90/"); + + response.setHeader("Content-Type", "text/plain", false); + response.setStatusLine("1.1", 200, "OK"); + + // note: httpd.js' "Response" class uses ',' (no space) for merge. + response.setHeader("httpdMerge", "bar1", false); + response.setHeader("httpdMerge", "bar2", true); + response.setHeader("httpdMerge", "bar3", true); + // Some special headers like Proxy-Authenticate merge with \n + response.setHeader("Proxy-Authenticate", "line 1", true); + response.setHeader("Proxy-Authenticate", "line 2", true); + response.setHeader("Proxy-Authenticate", "line 3", true); + + response.bodyOutputStream.write(httpbody, httpbody.length); + + if (dbg) { + print("============== serverHandler: out"); + } +} + +function checkRequestResponse(request, data, context) { + if (dbg) { + print("============== checkRequestResponse: in"); + } + + Assert.equal(channel.responseStatus, 200); + Assert.equal(channel.responseStatusText, "OK"); + Assert.ok(channel.requestSucceeded); + + var response = channel.getResponseHeader("httpdMerge"); + Assert.equal(response, "bar1,bar2,bar3"); + channel.setResponseHeader("httpdMerge", "bar", true); + Assert.equal(channel.getResponseHeader("httpdMerge"), "bar1,bar2,bar3, bar"); + + response = channel.getResponseHeader("Proxy-Authenticate"); + Assert.equal(response, "line 1\nline 2\nline 3"); + + channel.contentCharset = "UTF-8"; + Assert.equal(channel.contentCharset, "UTF-8"); + Assert.equal(channel.contentType, "text/plain"); + Assert.equal(channel.contentLength, httpbody.length); + Assert.equal(data, httpbody); + + httpserver.stop(do_test_finished); + if (dbg) { + print("============== checkRequestResponse: out"); + } +} diff --git a/netwerk/test/unit/test_head_request_no_response_body.js b/netwerk/test/unit/test_head_request_no_response_body.js new file mode 100644 index 0000000000..dc920e3a0b --- /dev/null +++ b/netwerk/test/unit/test_head_request_no_response_body.js @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/* + +Test that a response to HEAD method should not have a body. +1. Create a GET request and write the response into cache. +2. Create the second GET request with the same URI and see if the response is + from cache. +3. Create a HEAD request and test if we got a response with an empty body. + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const responseContent = "response body"; + +function test_handler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-control", "max-age=9999", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + if (metadata.method != "HEAD") { + response.bodyOutputStream.write(responseContent, responseContent.length); + } +} + +function make_channel(url, method) { + let channel = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + channel.requestMethod = method; + return channel; +} + +async function get_response(channel, fromCache) { + return new Promise(resolve => { + channel.asyncOpen( + new ChannelListener((request, buffer, ctx, isFromCache) => { + ok(fromCache == isFromCache, `got response from cache = ${fromCache}`); + resolve(buffer); + }) + ); + }); +} + +async function stop_server(httpserver) { + return new Promise(resolve => { + httpserver.stop(resolve); + }); +} + +add_task(async function () { + let httpserver = new HttpServer(); + httpserver.registerPathHandler("/testdir", test_handler); + httpserver.start(-1); + const PORT = httpserver.identity.primaryPort; + const URI = `http://localhost:${PORT}/testdir`; + + let response; + + response = await get_response(make_channel(URI, "GET"), false); + ok(response === responseContent, "got response body"); + + response = await get_response(make_channel(URI, "GET"), true); + ok(response === responseContent, "got response body from cache"); + + response = await get_response(make_channel(URI, "HEAD"), false); + ok(response === "", "should have empty body"); + + await stop_server(httpserver); +}); diff --git a/netwerk/test/unit/test_header_Accept-Language.js b/netwerk/test/unit/test_header_Accept-Language.js new file mode 100644 index 0000000000..b00e02d13b --- /dev/null +++ b/netwerk/test/unit/test_header_Accept-Language.js @@ -0,0 +1,99 @@ +/* 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/. */ + +// +// HTTP Accept-Language header test +// + +"use strict"; + +var testpath = "/bug672448"; + +function run_test() { + let intlPrefs = Services.prefs.getBranch("intl."); + + // Save old value of preference for later. + let oldPref = intlPrefs.getCharPref("accept_languages"); + + // Test different numbers of languages, to test different fractions. + let acceptLangTests = [ + "qaa", // 1 + "qaa,qab", // 2 + "qaa,qab,qac,qad", // 4 + "qaa,qab,qac,qad,qae,qaf,qag,qah", // 8 + "qaa,qab,qac,qad,qae,qaf,qag,qah,qai,qaj", // 10 + "qaa,qab,qac,qad,qae,qaf,qag,qah,qai,qaj,qak", // 11 + "qaa,qab,qac,qad,qae,qaf,qag,qah,qai,qaj,qak,qal,qam,qan,qao,qap,qaq,qar,qas,qat,qau", // 21 + oldPref, // Restore old value of preference (and test it). + ]; + + let acceptLangTestsNum = acceptLangTests.length; + + for (let i = 0; i < acceptLangTestsNum; i++) { + // Set preference to test value. + intlPrefs.setCharPref("accept_languages", acceptLangTests[i]); + + // Test value. + test_accepted_languages(); + } +} + +function test_accepted_languages() { + let channel = setupChannel(testpath); + + let AcceptLanguage = channel.getRequestHeader("Accept-Language"); + + let acceptedLanguages = AcceptLanguage.split(","); + + let acceptedLanguagesLength = acceptedLanguages.length; + + for (let i = 0; i < acceptedLanguagesLength; i++) { + let qualityValue; + + try { + // The q-value must conform to the definition in HTTP/1.1 Section 3.9. + [, , qualityValue] = acceptedLanguages[i] + .trim() + .match(/^([a-z0-9_-]*?)(?:;q=(1(?:\.0{0,3})?|0(?:\.[0-9]{0,3})))?$/i); + } catch (e) { + do_throw("Invalid language tag or quality value: " + e); + } + + if (i == 0) { + // The first language shouldn't have a quality value. + Assert.equal(qualityValue, undefined); + } else { + let decimalPlaces; + + // When the number of languages is small, we keep the quality value to only one decimal place. + // Otherwise, it can be up to two decimal places. + if (acceptedLanguagesLength < 10) { + Assert.ok(qualityValue.length == 3); + + decimalPlaces = 1; + } else { + Assert.ok(qualityValue.length >= 3); + Assert.ok(qualityValue.length <= 4); + + decimalPlaces = 2; + } + + // All the other languages should have an evenly-spaced quality value. + Assert.equal( + parseFloat(qualityValue).toFixed(decimalPlaces), + (1.0 - (1 / acceptedLanguagesLength) * i).toFixed(decimalPlaces) + ); + } + } +} + +function setupChannel(path) { + let chan = NetUtil.newChannel({ + uri: "http://localhost:4444" + path, + loadUsingSystemPrincipal: true, + }); + + chan.QueryInterface(Ci.nsIHttpChannel); + return chan; +} diff --git a/netwerk/test/unit/test_header_Accept-Language_case.js b/netwerk/test/unit/test_header_Accept-Language_case.js new file mode 100644 index 0000000000..69d936d74a --- /dev/null +++ b/netwerk/test/unit/test_header_Accept-Language_case.js @@ -0,0 +1,50 @@ +"use strict"; + +var testpath = "/bug1054739"; + +function run_test() { + let intlPrefs = Services.prefs.getBranch("intl."); + + let oldAcceptLangPref = intlPrefs.getCharPref("accept_languages"); + + let testData = [ + ["en", "en"], + ["ast", "ast"], + ["fr-ca", "fr-CA"], + ["zh-yue", "zh-yue"], + ["az-latn", "az-Latn"], + ["sl-nedis", "sl-nedis"], + ["zh-hant-hk", "zh-Hant-HK"], + ["ZH-HANT-HK", "zh-Hant-HK"], + ["en-us-x-priv", "en-US-x-priv"], + ["en-us-x-twain", "en-US-x-twain"], + ["de, en-US, en", "de,en-US;q=0.7,en;q=0.3"], + ["de,en-us,en", "de,en-US;q=0.7,en;q=0.3"], + ["en-US, en", "en-US,en;q=0.5"], + ["EN-US;q=0.2, EN", "en-US,en;q=0.5"], + ["en ;q=0.8, de ", "en,de;q=0.5"], + [",en,", "en"], + ]; + + for (let i = 0; i < testData.length; i++) { + let acceptLangPref = testData[i][0]; + let expectedHeader = testData[i][1]; + + intlPrefs.setCharPref("accept_languages", acceptLangPref); + let acceptLangHeader = + setupChannel(testpath).getRequestHeader("Accept-Language"); + equal(acceptLangHeader, expectedHeader); + } + + intlPrefs.setCharPref("accept_languages", oldAcceptLangPref); +} + +function setupChannel(path) { + let uri = NetUtil.newURI("http://localhost:4444" + path); + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + return chan; +} diff --git a/netwerk/test/unit/test_header_Server_Timing.js b/netwerk/test/unit/test_header_Server_Timing.js new file mode 100644 index 0000000000..0e65cf3ccf --- /dev/null +++ b/netwerk/test/unit/test_header_Server_Timing.js @@ -0,0 +1,64 @@ +/* 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/. */ + +// +// HTTP Server-Timing header test +// + +"use strict"; + +function make_and_open_channel(url, callback) { + let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); + chan.asyncOpen(new ChannelListener(callback, null, CL_ALLOW_UNKNOWN_CL)); +} + +var responseServerTiming = [ + { metric: "metric", duration: "123.4", description: "description" }, + { metric: "metric2", duration: "456.78", description: "description1" }, +]; +var trailerServerTiming = [ + { metric: "metric3", duration: "789.11", description: "description2" }, + { metric: "metric4", duration: "1112.13", description: "description3" }, +]; + +function run_test() { + do_test_pending(); + + // Set up to allow the cert presented by the server + do_get_profile(); + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.dns.localDomains"); + }); + + var serverPort = Services.env.get("MOZHTTP2_PORT"); + make_and_open_channel( + "https://foo.example.com:" + serverPort + "/server-timing", + readServerContent + ); +} + +function checkServerTimingContent(headers) { + var expectedResult = responseServerTiming.concat(trailerServerTiming); + Assert.equal(headers.length, expectedResult.length); + + for (var i = 0; i < expectedResult.length; i++) { + let header = headers.queryElementAt(i, Ci.nsIServerTiming); + Assert.equal(header.name, expectedResult[i].metric); + Assert.equal(header.description, expectedResult[i].description); + Assert.equal(header.duration, parseFloat(expectedResult[i].duration)); + } +} + +function readServerContent(request, buffer) { + let channel = request.QueryInterface(Ci.nsITimedChannel); + let headers = channel.serverTiming.QueryInterface(Ci.nsIArray); + checkServerTimingContent(headers); + do_test_finished(); +} diff --git a/netwerk/test/unit/test_headers.js b/netwerk/test/unit/test_headers.js new file mode 100644 index 0000000000..282f8a29e1 --- /dev/null +++ b/netwerk/test/unit/test_headers.js @@ -0,0 +1,182 @@ +// +// cleaner HTTP header test infrastructure +// +// tests bugs: 589292, [add more here: see hg log for definitive list] +// +// TO ADD NEW TESTS: +// 1) Increment up 'lastTest' to new number (say, "99") +// 2) Add new test 'handler99' and 'completeTest99' functions. +// 3) If your test should fail the necko channel, set +// test_flags[99] = CL_EXPECT_FAILURE. +// +// TO DEBUG JUST ONE TEST: temporarily change firstTest and lastTest to equal +// the test # you're interested in. +// +// For tests that need duplicate copies of headers to be sent, see +// test_duplicate_headers.js + +"use strict"; + +var firstTest = 1; // set to test of interest when debugging +var lastTest = 4; // set to test of interest when debugging +//////////////////////////////////////////////////////////////////////////////// + +// Note: sets Cc and Ci variables + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var nextTest = firstTest; +var test_flags = []; +var testPathBase = "/test_headers"; + +function run_test() { + httpserver.start(-1); + + do_test_pending(); + run_test_number(nextTest); +} + +function runNextTest() { + if (nextTest == lastTest) { + endTests(); + return; + } + nextTest++; + // Make sure test functions exist + if (globalThis["handler" + nextTest] == undefined) { + do_throw("handler" + nextTest + " undefined!"); + } + if (globalThis["completeTest" + nextTest] == undefined) { + do_throw("completeTest" + nextTest + " undefined!"); + } + + run_test_number(nextTest); +} + +function run_test_number(num) { + let testPath = testPathBase + num; + httpserver.registerPathHandler(testPath, globalThis["handler" + num]); + + var channel = setupChannel(testPath); + let flags = test_flags[num]; // OK if flags undefined for test + channel.asyncOpen( + new ChannelListener(globalThis["completeTest" + num], channel, flags) + ); +} + +function setupChannel(url) { + var chan = NetUtil.newChannel({ + uri: URL + url, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + return httpChan; +} + +function endTests() { + httpserver.stop(do_test_finished); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 1: test Content-Disposition channel attributes +// eslint-disable-next-line no-unused-vars +function handler1(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Disposition", "attachment; filename=foo"); + response.setHeader("Content-Type", "text/plain", false); +} + +// eslint-disable-next-line no-unused-vars +function completeTest1(request, data, ctx) { + try { + var chan = request.QueryInterface(Ci.nsIChannel); + Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); + Assert.equal(chan.contentDispositionFilename, "foo"); + Assert.equal(chan.contentDispositionHeader, "attachment; filename=foo"); + } catch (ex) { + do_throw("error parsing Content-Disposition: " + ex); + } + runNextTest(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 2: no filename +// eslint-disable-next-line no-unused-vars +function handler2(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Disposition", "attachment"); + var body = "foo"; + response.bodyOutputStream.write(body, body.length); +} + +// eslint-disable-next-line no-unused-vars +function completeTest2(request, data, ctx) { + try { + var chan = request.QueryInterface(Ci.nsIChannel); + Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); + Assert.equal(chan.contentDispositionHeader, "attachment"); + chan.contentDispositionFilename; // should barf + do_throw("Should have failed getting Content-Disposition filename"); + } catch (ex) { + info("correctly ate exception"); + } + runNextTest(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 3: filename missing +// eslint-disable-next-line no-unused-vars +function handler3(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Disposition", "attachment; filename="); + var body = "foo"; + response.bodyOutputStream.write(body, body.length); +} + +// eslint-disable-next-line no-unused-vars +function completeTest3(request, data, ctx) { + try { + var chan = request.QueryInterface(Ci.nsIChannel); + Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); + Assert.equal(chan.contentDispositionHeader, "attachment; filename="); + chan.contentDispositionFilename; // should barf + + do_throw("Should have failed getting Content-Disposition filename"); + } catch (ex) { + info("correctly ate exception"); + } + runNextTest(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 4: inline +// eslint-disable-next-line no-unused-vars +function handler4(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Disposition", "inline"); + var body = "foo"; + response.bodyOutputStream.write(body, body.length); +} + +// eslint-disable-next-line no-unused-vars +function completeTest4(request, data, ctx) { + try { + var chan = request.QueryInterface(Ci.nsIChannel); + Assert.equal(chan.contentDisposition, chan.DISPOSITION_INLINE); + Assert.equal(chan.contentDispositionHeader, "inline"); + + chan.contentDispositionFilename; // should barf + do_throw("Should have failed getting Content-Disposition filename"); + } catch (ex) { + info("correctly ate exception"); + } + runNextTest(); +} diff --git a/netwerk/test/unit/test_hostnameIsLocalIPAddress.js b/netwerk/test/unit/test_hostnameIsLocalIPAddress.js new file mode 100644 index 0000000000..64d246c633 --- /dev/null +++ b/netwerk/test/unit/test_hostnameIsLocalIPAddress.js @@ -0,0 +1,37 @@ +"use strict"; + +function run_test() { + let testURIs = [ + ["http://example.com", false], + ["about:robots", false], + // 10/8 prefix (RFC 1918) + ["http://9.255.255.255", false], + ["http://10.0.0.0", true], + ["http://10.0.23.31", true], + ["http://10.255.255.255", true], + ["http://11.0.0.0", false], + // 169.254/16 prefix (Link Local) + ["http://169.253.255.255", false], + ["http://169.254.0.0", true], + ["http://169.254.42.91", true], + ["http://169.254.255.255", true], + ["http://169.255.0.0", false], + // 172.16/12 prefix (RFC 1918) + ["http://172.15.255.255", false], + ["http://172.16.0.0", true], + ["http://172.25.110.0", true], + ["http://172.31.255.255", true], + ["http://172.32.0.0", false], + // 192.168/16 prefix (RFC 1918) + ["http://192.167.255.255", false], + ["http://192.168.0.0", true], + ["http://192.168.127.10", true], + ["http://192.168.255.255", true], + ["http://192.169.0.0", false], + ]; + + for (let [uri, isLocal] of testURIs) { + let nsuri = Services.io.newURI(uri); + equal(isLocal, Services.io.hostnameIsLocalIPAddress(nsuri)); + } +} diff --git a/netwerk/test/unit/test_hostnameIsSharedIPAddress.js b/netwerk/test/unit/test_hostnameIsSharedIPAddress.js new file mode 100644 index 0000000000..c3eaeabc0e --- /dev/null +++ b/netwerk/test/unit/test_hostnameIsSharedIPAddress.js @@ -0,0 +1,17 @@ +"use strict"; + +function run_test() { + let testURIs = [ + // 100.64/10 prefix (RFC 6598) + ["http://100.63.255.254", false], + ["http://100.64.0.0", true], + ["http://100.91.63.42", true], + ["http://100.127.255.254", true], + ["http://100.128.0.0", false], + ]; + + for (let [uri, isShared] of testURIs) { + let nsuri = Services.io.newURI(uri); + equal(isShared, Services.io.hostnameIsSharedIPAddress(nsuri)); + } +} diff --git a/netwerk/test/unit/test_http1-proxy.js b/netwerk/test/unit/test_http1-proxy.js new file mode 100644 index 0000000000..88faf7d884 --- /dev/null +++ b/netwerk/test/unit/test_http1-proxy.js @@ -0,0 +1,229 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 test checks following expectations when using HTTP/1 proxy: + * + * - check we are seeing expected nsresult error codes on channels + * (nsIChannel.status) corresponding to different proxy status code + * responses (502, 504, 407, ...) + * - check we don't try to ask for credentials or otherwise authenticate to + * the proxy when 407 is returned and there is no Proxy-Authenticate + * response header sent + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + +let server_port; +let http_server; + +class ProxyFilter { + constructor(type, host, port, flags) { + this._type = type; + this._host = host; + this._port = port; + this._flags = flags; + this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]); + } + applyFilter(uri, pi, cb) { + if (uri.spec.match(/(\/proxy-session-counter)/)) { + cb.onProxyFilterResult(pi); + return; + } + cb.onProxyFilterResult( + pps.newProxyInfo( + this._type, + this._host, + this._port, + "", + "", + this._flags, + 1000, + null + ) + ); + } +} + +class UnxpectedAuthPrompt2 { + constructor(signal) { + this.signal = signal; + this.QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt2"]); + } + asyncPromptAuth() { + this.signal.triggered = true; + throw Components.Exception("", Cr.ERROR_UNEXPECTED); + } +} + +class AuthRequestor { + constructor(prompt) { + this.prompt = prompt; + this.QueryInterface = ChromeUtils.generateQI(["nsIInterfaceRequestor"]); + } + getInterface(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + return this.prompt(); + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } +} + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + // Using TYPE_DOCUMENT for the authentication dialog test, it'd be blocked for other types + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); +} + +function get_response(channel, flags = CL_ALLOW_UNKNOWN_CL) { + return new Promise(resolve => { + channel.asyncOpen( + new ChannelListener( + (request, data) => { + request.QueryInterface(Ci.nsIHttpChannel); + const status = request.status; + const http_code = status ? undefined : request.responseStatus; + request.QueryInterface(Ci.nsIProxiedChannel); + const proxy_connect_response_code = + request.httpProxyConnectResponseCode; + resolve({ status, http_code, data, proxy_connect_response_code }); + }, + null, + flags + ) + ); + }); +} + +function connect_handler(request, response) { + Assert.equal(request.method, "CONNECT"); + + switch (request.host) { + case "404.example.com": + response.setStatusLine(request.httpVersion, 404, "Not found"); + break; + case "407.example.com": + response.setStatusLine(request.httpVersion, 407, "Authenticate"); + // And deliberately no Proxy-Authenticate header + break; + case "429.example.com": + response.setStatusLine(request.httpVersion, 429, "Too Many Requests"); + break; + case "502.example.com": + response.setStatusLine(request.httpVersion, 502, "Bad Gateway"); + break; + case "504.example.com": + response.setStatusLine(request.httpVersion, 504, "Gateway timeout"); + break; + default: + response.setStatusLine(request.httpVersion, 500, "I am dumb"); + } +} + +add_task(async function setup() { + http_server = new HttpServer(); + http_server.identity.add("https", "404.example.com", 443); + http_server.identity.add("https", "407.example.com", 443); + http_server.identity.add("https", "429.example.com", 443); + http_server.identity.add("https", "502.example.com", 443); + http_server.identity.add("https", "504.example.com", 443); + http_server.registerPathHandler("CONNECT", connect_handler); + http_server.start(-1); + server_port = http_server.identity.primaryPort; + + // make all native resolve calls "secretly" resolve localhost instead + Services.prefs.setBoolPref("network.dns.native-is-localhost", true); + + pps.registerFilter(new ProxyFilter("http", "localhost", server_port, 0), 10); +}); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.dns.native-is-localhost"); +}); + +/** + * Test series beginning. + */ + +// The proxy responses with 407 instead of 200 Connected, make sure we get a proper error +// code from the channel and not try to ask for any credentials. +add_task(async function proxy_auth_failure() { + const chan = make_channel(`https://407.example.com/`); + const auth_prompt = { triggered: false }; + chan.notificationCallbacks = new AuthRequestor( + () => new UnxpectedAuthPrompt2(auth_prompt) + ); + const { status, http_code, proxy_connect_response_code } = await get_response( + chan, + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_AUTHENTICATION_FAILED); + Assert.equal(proxy_connect_response_code, 407); + Assert.equal(http_code, undefined); + Assert.equal(auth_prompt.triggered, false, "Auth prompt didn't trigger"); +}); + +// 502 Bad gateway code returned by the proxy. +add_task(async function proxy_bad_gateway_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://502.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); + Assert.equal(proxy_connect_response_code, 502); + Assert.equal(http_code, undefined); +}); + +// 504 Gateway timeout code returned by the proxy. +add_task(async function proxy_gateway_timeout_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://504.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_GATEWAY_TIMEOUT); + Assert.equal(proxy_connect_response_code, 504); + Assert.equal(http_code, undefined); +}); + +// 404 Not Found means the proxy could not resolve the host. +add_task(async function proxy_host_not_found_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://404.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_UNKNOWN_HOST); + Assert.equal(proxy_connect_response_code, 404); + Assert.equal(http_code, undefined); +}); + +// 429 Too Many Requests means we sent too many requests. +add_task(async function proxy_too_many_requests_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://429.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_TOO_MANY_REQUESTS); + Assert.equal(proxy_connect_response_code, 429); + Assert.equal(http_code, undefined); +}); + +add_task(async function shutdown() { + await new Promise(resolve => { + http_server.stop(resolve); + }); +}); diff --git a/netwerk/test/unit/test_http2-proxy-failing.js b/netwerk/test/unit/test_http2-proxy-failing.js new file mode 100644 index 0000000000..8530f55373 --- /dev/null +++ b/netwerk/test/unit/test_http2-proxy-failing.js @@ -0,0 +1,174 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* Test stream failure on the session to the proxy: + * - Test the case the error closes the affected stream only + * - Test the case the error closes the whole session and cancels existing + * streams. + */ + +/* eslint-env node */ + +"use strict"; + +const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + +let filter; + +class ProxyFilter { + constructor(type, host, port, flags) { + this._type = type; + this._host = host; + this._port = port; + this._flags = flags; + this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]); + } + applyFilter(uri, pi, cb) { + cb.onProxyFilterResult( + pps.newProxyInfo( + this._type, + this._host, + this._port, + null, + null, + this._flags, + 1000, + null + ) + ); + } +} + +function createPrincipal(url) { + var ssm = Services.scriptSecurityManager; + try { + return ssm.createContentPrincipal(Services.io.newURI(url), {}); + } catch (e) { + return null; + } +} + +function make_channel(url) { + return Services.io.newChannelFromURIWithProxyFlags( + Services.io.newURI(url), + null, + 16, + null, + createPrincipal(url), + createPrincipal(url), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, + Ci.nsIContentPolicy.TYPE_OTHER + ); +} + +function get_response(channel, flags = CL_ALLOW_UNKNOWN_CL, delay = 0) { + return new Promise(resolve => { + var listener = new ChannelListener( + (request, data) => { + request.QueryInterface(Ci.nsIHttpChannel); + const status = request.status; + const http_code = status ? undefined : request.responseStatus; + request.QueryInterface(Ci.nsIProxiedChannel); + const proxy_connect_response_code = + request.httpProxyConnectResponseCode; + resolve({ status, http_code, data, proxy_connect_response_code }); + }, + null, + flags + ); + if (delay > 0) { + do_timeout(delay, function () { + channel.asyncOpen(listener); + }); + } else { + channel.asyncOpen(listener); + } + }); +} + +add_task(async function setup() { + // Set to allow the cert presented by our H2 server + do_get_profile(); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + let proxy_port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(proxy_port, null); + + Services.prefs.setBoolPref("network.http.http2.enabled", true); + // make all native resolve calls "secretly" resolve localhost instead + Services.prefs.setBoolPref("network.dns.native-is-localhost", true); + + filter = new ProxyFilter("https", "localhost", proxy_port, 16); + pps.registerFilter(filter, 10); +}); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.http2.enabled"); + Services.prefs.clearUserPref("network.dns.native-is-localhost"); + + pps.unregisterFilter(filter); +}); + +add_task( + async function proxy_server_stream_soft_failure_multiple_streams_not_affected() { + let should_succeed = get_response(make_channel(`http://750.example.com`)); + const failed = await get_response( + make_channel(`http://illegalhpacksoft.example.com`), + CL_EXPECT_FAILURE, + 20 + ); + + const succeeded = await should_succeed; + + Assert.equal(failed.status, Cr.NS_ERROR_ILLEGAL_VALUE); + Assert.equal(failed.proxy_connect_response_code, 0); + Assert.equal(failed.http_code, undefined); + Assert.equal(succeeded.status, Cr.NS_OK); + Assert.equal(succeeded.proxy_connect_response_code, 200); + Assert.equal(succeeded.http_code, 200); + } +); + +add_task( + async function proxy_server_stream_hard_failure_multiple_streams_affected() { + let should_failed = get_response( + make_channel(`http://750.example.com`), + CL_EXPECT_FAILURE + ); + const failed1 = await get_response( + make_channel(`http://illegalhpackhard.example.com`), + CL_EXPECT_FAILURE + ); + + const failed2 = await should_failed; + + Assert.equal(failed1.status, 0x804b0053); + Assert.equal(failed1.proxy_connect_response_code, 0); + Assert.equal(failed1.http_code, undefined); + Assert.equal(failed2.status, 0x804b0053); + Assert.equal(failed2.proxy_connect_response_code, 0); + Assert.equal(failed2.http_code, undefined); + } +); + +add_task(async function test_http2_h11required_stream() { + let should_failed = await get_response( + make_channel(`http://h11required.com`), + CL_EXPECT_FAILURE + ); + + // See HTTP/1.1 connect handler in moz-http2.js. The handler returns + // "404 Not Found", so the expected error code is NS_ERROR_UNKNOWN_HOST. + Assert.equal(should_failed.status, Cr.NS_ERROR_UNKNOWN_HOST); + Assert.equal(should_failed.proxy_connect_response_code, 404); + Assert.equal(should_failed.http_code, undefined); +}); diff --git a/netwerk/test/unit/test_http2-proxy.js b/netwerk/test/unit/test_http2-proxy.js new file mode 100644 index 0000000000..5dbdb0262a --- /dev/null +++ b/netwerk/test/unit/test_http2-proxy.js @@ -0,0 +1,862 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 test checks following expectations when using HTTP/2 proxy: + * + * - when we request https access, we don't create different sessions for + * different origins, only new tunnels inside a single session + * - when the isolation key (`proxy_isolation`) is changed, new single session + * is created for new requests to same origins as before + * - error code returned from the tunnel (a proxy error - not end-server + * error!) doesn't kill the existing session + * - check we are seeing expected nsresult error codes on channels + * (nsIChannel.status) corresponding to different proxy status code + * responses (502, 504, 407, ...) + * - check we don't try to ask for credentials or otherwise authenticate to + * the proxy when 407 is returned and there is no Proxy-Authenticate + * response header sent + * - a stream reset for a connect stream to the proxy does not cause session to + * be closed and the request through the proxy will failed. + * - a "soft" stream error on a connection to the origin server will close the + * stream, but it will not close niether the HTTP/2 session to the proxy nor + * to the origin server. + * - a "hard" stream error on a connection to the origin server will close the + * HTTP/2 session to the origin server, but it will not close the HTTP/2 + * session to the proxy. + */ + +/* eslint-env node */ +/* global serverPort */ + +"use strict"; + +const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + +let proxy_port; +let filter; +let proxy; + +// See moz-http2 +const proxy_auth = "authorization-token"; +let proxy_isolation; + +class ProxyFilter { + constructor(type, host, port, flags) { + this._type = type; + this._host = host; + this._port = port; + this._flags = flags; + this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]); + } + applyFilter(uri, pi, cb) { + cb.onProxyFilterResult( + pps.newProxyInfo( + this._type, + this._host, + this._port, + proxy_auth, + proxy_isolation, + this._flags, + 1000, + null + ) + ); + } +} + +class UnxpectedAuthPrompt2 { + constructor(signal) { + this.signal = signal; + this.QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt2"]); + } + asyncPromptAuth() { + this.signal.triggered = true; + throw Components.Exception("", Cr.ERROR_UNEXPECTED); + } +} + +class SimpleAuthPrompt2 { + constructor(signal) { + this.signal = signal; + this.QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt2"]); + } + asyncPromptAuth(channel, callback, context, encryptionLevel, authInfo) { + this.signal.triggered = true; + executeSoon(function () { + authInfo.username = "user"; + authInfo.password = "pass"; + callback.onAuthAvailable(context, authInfo); + }); + } +} + +class AuthRequestor { + constructor(prompt) { + this.prompt = prompt; + this.QueryInterface = ChromeUtils.generateQI(["nsIInterfaceRequestor"]); + } + getInterface(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + return this.prompt(); + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } +} + +function createPrincipal(url) { + var ssm = Services.scriptSecurityManager; + try { + return ssm.createContentPrincipal(Services.io.newURI(url), {}); + } catch (e) { + return null; + } +} + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: createPrincipal(url), + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, + // Using TYPE_DOCUMENT for the authentication dialog test, it'd be blocked for other types + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); +} + +function get_response(channel, flags = CL_ALLOW_UNKNOWN_CL, delay = 0) { + return new Promise(resolve => { + var listener = new ChannelListener( + (request, data) => { + request.QueryInterface(Ci.nsIHttpChannel); + const status = request.status; + const http_code = status ? undefined : request.responseStatus; + request.QueryInterface(Ci.nsIProxiedChannel); + const proxy_connect_response_code = + request.httpProxyConnectResponseCode; + resolve({ status, http_code, data, proxy_connect_response_code }); + }, + null, + flags + ); + if (delay > 0) { + do_timeout(delay, function () { + channel.asyncOpen(listener); + }); + } else { + channel.asyncOpen(listener); + } + }); +} + +let initial_session_count = 0; + +class http2ProxyCode { + static listen(server, envport) { + if (!server) { + return Promise.resolve(0); + } + + let portSelection = 0; + if (envport !== undefined) { + try { + portSelection = parseInt(envport, 10); + } catch (e) { + portSelection = -1; + } + } + return new Promise(resolve => { + server.listen(portSelection, "0.0.0.0", 2000, () => { + resolve(server.address().port); + }); + }); + } + + static startNewProxy() { + const fs = require("fs"); + const options = { + key: fs.readFileSync(__dirname + "/http2-cert.key"), + cert: fs.readFileSync(__dirname + "/http2-cert.pem"), + }; + const http2 = require("http2"); + global.proxy = http2.createSecureServer(options); + this.setupProxy(); + return http2ProxyCode.listen(proxy).then(port => { + return { port, success: true }; + }); + } + + static closeProxy() { + proxy.closeSockets(); + return new Promise(resolve => { + proxy.close(resolve); + }); + } + + static proxySessionCount() { + if (!proxy) { + return 0; + } + return proxy.proxy_session_count; + } + + static proxySessionToOriginServersCount() { + if (!proxy) { + return 0; + } + return proxy.sessionToOriginServersCount; + } + + static setupProxy() { + if (!proxy) { + throw new Error("proxy is null"); + } + proxy.proxy_session_count = 0; + proxy.sessionToOriginServersCount = 0; + proxy.on("session", () => { + ++proxy.proxy_session_count; + }); + + // We need to track active connections so we can forcefully close keep-alive + // connections when shutting down the proxy. + proxy.socketIndex = 0; + proxy.socketMap = {}; + proxy.on("connection", function (socket) { + let index = proxy.socketIndex++; + proxy.socketMap[index] = socket; + socket.on("close", function () { + delete proxy.socketMap[index]; + }); + }); + proxy.closeSockets = function () { + for (let i in proxy.socketMap) { + proxy.socketMap[i].destroy(); + } + }; + + proxy.on("stream", (stream, headers) => { + if (headers[":method"] !== "CONNECT") { + // Only accept CONNECT requests + stream.respond({ ":status": 405 }); + stream.end(); + return; + } + + const target = headers[":authority"]; + + const authorization_token = headers["proxy-authorization"]; + if (target == "407.example.com:443") { + stream.respond({ ":status": 407 }); + // Deliberately send no Proxy-Authenticate header + stream.end(); + return; + } + if (target == "407.basic.example.com:443") { + // we want to return a different response than 407 to not re-request + // credentials (and thus loop) but also not 200 to not let the channel + // attempt to waste time connecting a non-existing https server - hence + // 418 I'm a teapot :) + if ("Basic dXNlcjpwYXNz" == authorization_token) { + stream.respond({ ":status": 418 }); + stream.end(); + return; + } + stream.respond({ + ":status": 407, + "proxy-authenticate": "Basic realm='foo'", + }); + stream.end(); + return; + } + if (target == "404.example.com:443") { + // 404 Not Found, a response code that a proxy should return when the host can't be found + stream.respond({ ":status": 404 }); + stream.end(); + return; + } + if (target == "429.example.com:443") { + // 429 Too Many Requests, a response code that a proxy should return when receiving too many requests + stream.respond({ ":status": 429 }); + stream.end(); + return; + } + if (target == "502.example.com:443") { + // 502 Bad Gateway, a response code mostly resembling immediate connection error + stream.respond({ ":status": 502 }); + stream.end(); + return; + } + if (target == "504.example.com:443") { + // 504 Gateway Timeout, did not receive a timely response from an upstream server + stream.respond({ ":status": 504 }); + stream.end(); + return; + } + if (target == "reset.example.com:443") { + // always reset the stream. + stream.close(0x0); + return; + } + + ++proxy.sessionToOriginServersCount; + const net = require("net"); + const socket = net.connect(serverPort, "127.0.0.1", () => { + try { + stream.respond({ ":status": 200 }); + socket.pipe(stream); + stream.pipe(socket); + } catch (exception) { + console.log(exception); + stream.close(); + } + }); + socket.on("error", error => { + throw new Error( + `Unexpected error when conneting the HTTP/2 server from the HTTP/2 proxy during CONNECT handling: '${error}'` + ); + }); + }); + } +} + +async function proxy_session_counter() { + let data = await NodeServer.execute( + processId, + `http2ProxyCode.proxySessionCount()` + ); + return parseInt(data) - initial_session_count; +} +async function proxy_session_to_origin_server_counter() { + let data = await NodeServer.execute( + processId, + `http2ProxyCode.proxySessionToOriginServersCount()` + ); + return parseInt(data) - initial_session_count; +} +let processId; +add_task(async function setup() { + // Set to allow the cert presented by our H2 server + do_get_profile(); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + let server_port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(server_port, null); + processId = await NodeServer.fork(); + await NodeServer.execute(processId, `serverPort = ${server_port}`); + await NodeServer.execute(processId, http2ProxyCode); + let proxy = await NodeServer.execute( + processId, + `http2ProxyCode.startNewProxy()` + ); + proxy_port = proxy.port; + Assert.notEqual(proxy_port, null); + + Services.prefs.setStringPref( + "services.settings.server", + `data:,#remote-settings-dummy/v1` + ); + + Services.prefs.setBoolPref("network.http.http2.enabled", true); + + // Even with network state isolation active, we don't end up using the + // partitioned principal. + Services.prefs.setBoolPref("privacy.partition.network_state", true); + + // make all native resolve calls "secretly" resolve localhost instead + Services.prefs.setBoolPref("network.dns.native-is-localhost", true); + + filter = new ProxyFilter("https", "localhost", proxy_port, 0); + pps.registerFilter(filter, 10); + + initial_session_count = await proxy_session_counter(); + info(`Initial proxy session count = ${initial_session_count}`); +}); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("services.settings.server"); + Services.prefs.clearUserPref("network.http.http2.enabled"); + Services.prefs.clearUserPref("network.dns.native-is-localhost"); + + pps.unregisterFilter(filter); + + await NodeServer.execute(processId, `http2ProxyCode.closeProxy()`); + await NodeServer.kill(processId); +}); + +/** + * Test series beginning. + */ + +// Check we reach the h2 end server and keep only one session with the proxy for two different origin. +// Here we use the first isolation token. +add_task(async function proxy_success_one_session() { + proxy_isolation = "TOKEN1"; + + const foo = await get_response( + make_channel(`https://foo.example.com/random-request-1`) + ); + const alt1 = await get_response( + make_channel(`https://alt1.example.com/random-request-2`) + ); + + Assert.equal(foo.status, Cr.NS_OK); + Assert.equal(foo.proxy_connect_response_code, 200); + Assert.equal(foo.http_code, 200); + Assert.ok(foo.data.match("random-request-1")); + Assert.ok(foo.data.match("You Win!")); + Assert.equal(alt1.status, Cr.NS_OK); + Assert.equal(alt1.proxy_connect_response_code, 200); + Assert.equal(alt1.http_code, 200); + Assert.ok(alt1.data.match("random-request-2")); + Assert.ok(alt1.data.match("You Win!")); + Assert.equal( + await proxy_session_counter(), + 1, + "Created just one session with the proxy" + ); +}); + +// The proxy responses with 407 instead of 200 Connected, make sure we get a proper error +// code from the channel and not try to ask for any credentials. +add_task(async function proxy_auth_failure() { + const chan = make_channel(`https://407.example.com/`); + const auth_prompt = { triggered: false }; + chan.notificationCallbacks = new AuthRequestor( + () => new UnxpectedAuthPrompt2(auth_prompt) + ); + const { status, http_code, proxy_connect_response_code } = await get_response( + chan, + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_AUTHENTICATION_FAILED); + Assert.equal(proxy_connect_response_code, 407); + Assert.equal(http_code, undefined); + Assert.equal(auth_prompt.triggered, false, "Auth prompt didn't trigger"); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 407" + ); +}); + +// The proxy responses with 407 with Proxy-Authenticate header presence. Make +// sure that we prompt the auth prompt to ask for credentials. +add_task(async function proxy_auth_basic() { + const chan = make_channel(`https://407.basic.example.com/`); + const auth_prompt = { triggered: false }; + chan.notificationCallbacks = new AuthRequestor( + () => new SimpleAuthPrompt2(auth_prompt) + ); + const { status, http_code, proxy_connect_response_code } = await get_response( + chan, + CL_EXPECT_FAILURE + ); + + // 418 indicates we pass the basic authentication. + Assert.equal(status, Cr.NS_ERROR_PROXY_CONNECTION_REFUSED); + Assert.equal(proxy_connect_response_code, 418); + Assert.equal(http_code, undefined); + Assert.equal(auth_prompt.triggered, true, "Auth prompt should trigger"); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 407" + ); +}); + +// 502 Bad gateway code returned by the proxy, still one session only, proper different code +// from the channel. +add_task(async function proxy_bad_gateway_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://502.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); + Assert.equal(proxy_connect_response_code, 502); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 502 after 407" + ); +}); + +// Second 502 Bad gateway code returned by the proxy, still one session only with the proxy. +add_task(async function proxy_bad_gateway_failure_two() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://502.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); + Assert.equal(proxy_connect_response_code, 502); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by second 502" + ); +}); + +// 504 Gateway timeout code returned by the proxy, still one session only, proper different code +// from the channel. +add_task(async function proxy_gateway_timeout_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://504.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_GATEWAY_TIMEOUT); + Assert.equal(proxy_connect_response_code, 504); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 504 after 502" + ); +}); + +// 404 Not Found means the proxy could not resolve the host. As for other error responses +// we still expect this not to close the existing session. +add_task(async function proxy_host_not_found_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://404.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_UNKNOWN_HOST); + Assert.equal(proxy_connect_response_code, 404); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 404 after 504" + ); +}); + +add_task(async function proxy_too_many_requests_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://429.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_TOO_MANY_REQUESTS); + Assert.equal(proxy_connect_response_code, 429); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 429 after 504" + ); +}); + +add_task(async function proxy_stream_reset_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://reset.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_NET_INTERRUPT); + Assert.equal(proxy_connect_response_code, 0); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 429 after 504" + ); +}); + +// The soft errors are not closing the session. +add_task(async function origin_server_stream_soft_failure() { + var current_num_sessions_to_origin_server = + await proxy_session_to_origin_server_counter(); + + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://foo.example.com/illegalhpacksoft`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_ILLEGAL_VALUE); + Assert.equal(proxy_connect_response_code, 200); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 1, + "No session to the proxy closed by soft stream errors" + ); + Assert.equal( + await proxy_session_to_origin_server_counter(), + current_num_sessions_to_origin_server, + "No session to the origin server closed by soft stream errors" + ); +}); + +// The soft errors are not closing the session. +add_task( + async function origin_server_stream_soft_failure_multiple_streams_not_affected() { + var current_num_sessions_to_origin_server = + await proxy_session_to_origin_server_counter(); + + let should_succeed = get_response( + make_channel(`https://foo.example.com/750ms`) + ); + + const failed = await get_response( + make_channel(`https://foo.example.com/illegalhpacksoft`), + CL_EXPECT_FAILURE, + 20 + ); + + const succeeded = await should_succeed; + + Assert.equal(failed.status, Cr.NS_ERROR_ILLEGAL_VALUE); + Assert.equal(failed.proxy_connect_response_code, 200); + Assert.equal(failed.http_code, undefined); + Assert.equal(succeeded.status, Cr.NS_OK); + Assert.equal(succeeded.proxy_connect_response_code, 200); + Assert.equal(succeeded.http_code, 200); + Assert.equal( + await proxy_session_counter(), + 1, + "No session to the proxy closed by soft stream errors" + ); + Assert.equal( + await proxy_session_to_origin_server_counter(), + current_num_sessions_to_origin_server, + "No session to the origin server closed by soft stream errors" + ); + } +); + +// Make sure that the above error codes don't kill the session to the proxy. +add_task(async function proxy_success_still_one_session() { + const foo = await get_response( + make_channel(`https://foo.example.com/random-request-1`) + ); + const alt1 = await get_response( + make_channel(`https://alt1.example.com/random-request-2`) + ); + + Assert.equal(foo.status, Cr.NS_OK); + Assert.equal(foo.http_code, 200); + Assert.equal(foo.proxy_connect_response_code, 200); + Assert.ok(foo.data.match("random-request-1")); + Assert.equal(alt1.status, Cr.NS_OK); + Assert.equal(alt1.proxy_connect_response_code, 200); + Assert.equal(alt1.http_code, 200); + Assert.ok(alt1.data.match("random-request-2")); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session to the proxy created after stream error codes" + ); +}); + +// Have a new isolation key, this means we are expected to create a new, and again one only, +// session with the proxy to reach the end server. +add_task(async function proxy_success_isolated_session() { + Assert.notEqual(proxy_isolation, "TOKEN2"); + proxy_isolation = "TOKEN2"; + + const foo = await get_response( + make_channel(`https://foo.example.com/random-request-1`) + ); + const alt1 = await get_response( + make_channel(`https://alt1.example.com/random-request-2`) + ); + const lh = await get_response( + make_channel(`https://localhost/random-request-3`) + ); + + Assert.equal(foo.status, Cr.NS_OK); + Assert.equal(foo.proxy_connect_response_code, 200); + Assert.equal(foo.http_code, 200); + Assert.ok(foo.data.match("random-request-1")); + Assert.ok(foo.data.match("You Win!")); + Assert.equal(alt1.status, Cr.NS_OK); + Assert.equal(alt1.proxy_connect_response_code, 200); + Assert.equal(alt1.http_code, 200); + Assert.ok(alt1.data.match("random-request-2")); + Assert.ok(alt1.data.match("You Win!")); + Assert.equal(lh.status, Cr.NS_OK); + Assert.equal(lh.proxy_connect_response_code, 200); + Assert.equal(lh.http_code, 200); + Assert.ok(lh.data.match("random-request-3")); + Assert.ok(lh.data.match("You Win!")); + Assert.equal( + await proxy_session_counter(), + 2, + "Just one new session seen after changing the isolation key" + ); +}); + +// Check that error codes are still handled the same way with new isolation, just in case. +add_task(async function proxy_bad_gateway_failure_isolated() { + const failure1 = await get_response( + make_channel(`https://502.example.com/`), + CL_EXPECT_FAILURE + ); + const failure2 = await get_response( + make_channel(`https://502.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(failure1.status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); + Assert.equal(failure1.proxy_connect_response_code, 502); + Assert.equal(failure1.http_code, undefined); + Assert.equal(failure2.status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); + Assert.equal(failure2.proxy_connect_response_code, 502); + Assert.equal(failure2.http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 2, + "No new session created by 502" + ); +}); + +add_task(async function proxy_success_check_number_of_session() { + const foo = await get_response( + make_channel(`https://foo.example.com/random-request-1`) + ); + const alt1 = await get_response( + make_channel(`https://alt1.example.com/random-request-2`) + ); + const lh = await get_response( + make_channel(`https://localhost/random-request-3`) + ); + + Assert.equal(foo.status, Cr.NS_OK); + Assert.equal(foo.proxy_connect_response_code, 200); + Assert.equal(foo.http_code, 200); + Assert.ok(foo.data.match("random-request-1")); + Assert.ok(foo.data.match("You Win!")); + Assert.equal(alt1.status, Cr.NS_OK); + Assert.equal(alt1.proxy_connect_response_code, 200); + Assert.equal(alt1.http_code, 200); + Assert.ok(alt1.data.match("random-request-2")); + Assert.ok(alt1.data.match("You Win!")); + Assert.equal(lh.status, Cr.NS_OK); + Assert.equal(lh.proxy_connect_response_code, 200); + Assert.equal(lh.http_code, 200); + Assert.ok(lh.data.match("random-request-3")); + Assert.ok(lh.data.match("You Win!")); + Assert.equal( + await proxy_session_counter(), + 2, + "The number of sessions has not changed" + ); +}); + +// The hard errors are closing the session. +add_task(async function origin_server_stream_hard_failure() { + var current_num_sessions_to_origin_server = + await proxy_session_to_origin_server_counter(); + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://foo.example.com/illegalhpackhard`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, 0x804b0053); + Assert.equal(proxy_connect_response_code, 200); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 2, + "No new session to the proxy." + ); + Assert.equal( + await proxy_session_to_origin_server_counter(), + current_num_sessions_to_origin_server, + "No new session to the origin server yet." + ); + + // Check the a new session ill be opened. + const foo = await get_response( + make_channel(`https://foo.example.com/random-request-1`) + ); + + Assert.equal(foo.status, Cr.NS_OK); + Assert.equal(foo.proxy_connect_response_code, 200); + Assert.equal(foo.http_code, 200); + Assert.ok(foo.data.match("random-request-1")); + Assert.ok(foo.data.match("You Win!")); + + Assert.equal( + await proxy_session_counter(), + 2, + "No new session to the proxy is created after a hard stream failure on the session to the origin server." + ); + Assert.equal( + await proxy_session_to_origin_server_counter(), + current_num_sessions_to_origin_server + 1, + "A new session to the origin server after a hard stream error" + ); +}); + +// The hard errors are closing the session. +add_task( + async function origin_server_stream_hard_failure_multiple_streams_affected() { + var current_num_sessions_to_origin_server = + await proxy_session_to_origin_server_counter(); + let should_fail = get_response( + make_channel(`https://foo.example.com/750msNoData`), + CL_EXPECT_FAILURE + ); + const failed1 = await get_response( + make_channel(`https://foo.example.com/illegalhpackhard`), + CL_EXPECT_FAILURE, + 10 + ); + + const failed2 = await should_fail; + + Assert.equal(failed1.status, 0x804b0053); + Assert.equal(failed1.proxy_connect_response_code, 200); + Assert.equal(failed1.http_code, undefined); + Assert.equal(failed2.status, 0x804b0053); + Assert.equal(failed2.proxy_connect_response_code, 200); + Assert.equal(failed2.http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 2, + "No new session to the proxy" + ); + Assert.equal( + await proxy_session_to_origin_server_counter(), + current_num_sessions_to_origin_server, + "No session to the origin server yet." + ); + // Check the a new session ill be opened. + const foo = await get_response( + make_channel(`https://foo.example.com/random-request-1`) + ); + + Assert.equal(foo.status, Cr.NS_OK); + Assert.equal(foo.proxy_connect_response_code, 200); + Assert.equal(foo.http_code, 200); + Assert.ok(foo.data.match("random-request-1")); + Assert.ok(foo.data.match("You Win!")); + + Assert.equal( + await proxy_session_counter(), + 2, + "No new session to the proxy is created after a hard stream failure on the session to the origin server." + ); + + Assert.equal( + await proxy_session_to_origin_server_counter(), + current_num_sessions_to_origin_server + 1, + "A new session to the origin server after a hard stream error" + ); + } +); diff --git a/netwerk/test/unit/test_http2.js b/netwerk/test/unit/test_http2.js new file mode 100644 index 0000000000..1324527db6 --- /dev/null +++ b/netwerk/test/unit/test_http2.js @@ -0,0 +1,479 @@ +/* import-globals-from http2_test_common.js */ + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var concurrent_channels = []; +var httpserv = null; +var httpserv2 = null; + +var loadGroup; +var serverPort; + +function altsvcHttp1Server(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Connection", "close", false); + response.setHeader("Alt-Svc", 'h2=":' + serverPort + '"', false); + var body = "this is where a cool kid would write something neat.\n"; + response.bodyOutputStream.write(body, body.length); +} + +function h1ServerWK(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + + var body = '["http://foo.example.com:' + httpserv.identity.primaryPort + '"]'; + response.bodyOutputStream.write(body, body.length); +} + +function altsvcHttp1Server2(metadata, response) { + // this server should never be used thanks to an alt svc frame from the + // h2 server.. but in case of some async lag in setting the alt svc route + // up we have it. + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Connection", "close", false); + var body = "hanging.\n"; + response.bodyOutputStream.write(body, body.length); +} + +function h1ServerWK2(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + + var body = + '["http://foo.example.com:' + httpserv2.identity.primaryPort + '"]'; + response.bodyOutputStream.write(body, body.length); +} + +add_setup(async function setup() { + serverPort = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(serverPort, null); + dump("using port " + serverPort + "\n"); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. Some older tests in + // this suite use localhost with a TOFU exception, but new ones should use + // foo.example.com + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + Services.prefs.setBoolPref("network.http.http2.enabled", true); + Services.prefs.setBoolPref("network.http.http2.allow-push", true); + Services.prefs.setBoolPref("network.http.altsvc.enabled", true); + Services.prefs.setBoolPref("network.http.altsvc.oe", true); + Services.prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, bar.example.com" + ); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + + httpserv = new HttpServer(); + httpserv.registerPathHandler("/altsvc1", altsvcHttp1Server); + httpserv.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK); + httpserv.start(-1); + httpserv.identity.setPrimary( + "http", + "foo.example.com", + httpserv.identity.primaryPort + ); + + httpserv2 = new HttpServer(); + httpserv2.registerPathHandler("/altsvc2", altsvcHttp1Server2); + httpserv2.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK2); + httpserv2.start(-1); + httpserv2.identity.setPrimary( + "http", + "foo.example.com", + httpserv2.identity.primaryPort + ); +}); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); + Services.prefs.clearUserPref("network.http.http2.enabled"); + Services.prefs.clearUserPref("network.http.http2.allow-push"); + Services.prefs.clearUserPref("network.http.altsvc.enabled"); + Services.prefs.clearUserPref("network.http.altsvc.oe"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref( + "network.cookieJarSettings.unblocked_for_testing" + ); + await httpserv.stop(); + await httpserv2.stop(); +}); + +// hack - the header test resets the multiplex object on the server, +// so make sure header is always run before the multiplex test. +// +// make sure post_big runs first to test race condition in restarting +// a stalled stream when a SETTINGS frame arrives +add_task(async function do_test_http2_post_big() { + const { httpProxyConnectResponseCode } = await test_http2_post_big( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_basic() { + const { httpProxyConnectResponseCode } = await test_http2_basic(serverPort); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_concurrent() { + const { httpProxyConnectResponseCode } = await test_http2_concurrent( + concurrent_channels, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_concurrent_post() { + const { httpProxyConnectResponseCode } = await test_http2_concurrent_post( + concurrent_channels, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_basic_unblocked_dep() { + const { httpProxyConnectResponseCode } = await test_http2_basic_unblocked_dep( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_nospdy() { + const { httpProxyConnectResponseCode } = await test_http2_nospdy(serverPort); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_push1() { + const { httpProxyConnectResponseCode } = await test_http2_push1( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_push2() { + const { httpProxyConnectResponseCode } = await test_http2_push2( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_push3() { + const { httpProxyConnectResponseCode } = await test_http2_push3( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_push4() { + const { httpProxyConnectResponseCode } = await test_http2_push4( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_push5() { + const { httpProxyConnectResponseCode } = await test_http2_push5( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_push6() { + const { httpProxyConnectResponseCode } = await test_http2_push6( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_altsvc() { + const { httpProxyConnectResponseCode } = await test_http2_altsvc( + httpserv.identity.primaryPort, + httpserv2.identity.primaryPort, + false + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_doubleheader() { + const { httpProxyConnectResponseCode } = await test_http2_doubleheader( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_xhr() { + await test_http2_xhr(serverPort); +}); + +add_task(async function do_test_http2_header() { + const { httpProxyConnectResponseCode } = await test_http2_header(serverPort); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_invalid_response_header_name_spaces() { + const { httpProxyConnectResponseCode } = + await test_http2_invalid_response_header(serverPort, "name_spaces"); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task( + async function do_test_http2_invalid_response_header_value_line_feed() { + const { httpProxyConnectResponseCode } = + await test_http2_invalid_response_header(serverPort, "value_line_feed"); + Assert.equal(httpProxyConnectResponseCode, -1); + } +); + +add_task( + async function do_test_http2_invalid_response_header_value_carriage_return() { + const { httpProxyConnectResponseCode } = + await test_http2_invalid_response_header( + serverPort, + "value_carriage_return" + ); + Assert.equal(httpProxyConnectResponseCode, -1); + } +); + +add_task(async function do_test_http2_invalid_response_header_value_null() { + const { httpProxyConnectResponseCode } = + await test_http2_invalid_response_header(serverPort, "value_null"); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_cookie_crumbling() { + const { httpProxyConnectResponseCode } = await test_http2_cookie_crumbling( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_multiplex() { + var values = await test_http2_multiplex(serverPort); + Assert.equal(values[0].httpProxyConnectResponseCode, -1); + Assert.equal(values[1].httpProxyConnectResponseCode, -1); + Assert.notEqual(values[0].streamID, values[1].streamID); +}); + +add_task(async function do_test_http2_big() { + const { httpProxyConnectResponseCode } = await test_http2_big(serverPort); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_huge_suspended() { + const { httpProxyConnectResponseCode } = await test_http2_huge_suspended( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_post() { + const { httpProxyConnectResponseCode } = await test_http2_post(serverPort); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_empty_post() { + const { httpProxyConnectResponseCode } = await test_http2_empty_post( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_patch() { + const { httpProxyConnectResponseCode } = await test_http2_patch(serverPort); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_pushapi_1() { + const { httpProxyConnectResponseCode } = await test_http2_pushapi_1( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_continuations() { + const { httpProxyConnectResponseCode } = await test_http2_continuations( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_blocking_download() { + const { httpProxyConnectResponseCode } = await test_http2_blocking_download( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_illegalhpacksoft() { + const { httpProxyConnectResponseCode } = await test_http2_illegalhpacksoft( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_illegalhpackhard() { + const { httpProxyConnectResponseCode } = await test_http2_illegalhpackhard( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_folded_header() { + const { httpProxyConnectResponseCode } = await test_http2_folded_header( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_empty_data() { + const { httpProxyConnectResponseCode } = await test_http2_empty_data( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_status_phrase() { + const { httpProxyConnectResponseCode } = await test_http2_status_phrase( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_doublepush() { + const { httpProxyConnectResponseCode } = await test_http2_doublepush( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_disk_cache_push() { + const { httpProxyConnectResponseCode } = await test_http2_disk_cache_push( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_h11required_stream() { + // Add new tests above here - best to add new tests before h1 + // streams get too involved + // These next two must always come in this order + const { httpProxyConnectResponseCode } = await test_http2_h11required_stream( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_h11required_session() { + const { httpProxyConnectResponseCode } = await test_http2_h11required_session( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_retry_rst() { + const { httpProxyConnectResponseCode } = await test_http2_retry_rst( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_wrongsuite_tls12() { + const { httpProxyConnectResponseCode } = await test_http2_wrongsuite_tls12( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_wrongsuite_tls13() { + const { httpProxyConnectResponseCode } = await test_http2_wrongsuite_tls13( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_push_firstparty1() { + const { httpProxyConnectResponseCode } = await test_http2_push_firstparty1( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_push_firstparty2() { + const { httpProxyConnectResponseCode } = await test_http2_push_firstparty2( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_push_firstparty3() { + const { httpProxyConnectResponseCode } = await test_http2_push_firstparty3( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_push_userContext1() { + const { httpProxyConnectResponseCode } = await test_http2_push_userContext1( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_push_userContext2() { + const { httpProxyConnectResponseCode } = await test_http2_push_userContext2( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); + +add_task(async function do_test_http2_push_userContext3() { + const { httpProxyConnectResponseCode } = await test_http2_push_userContext3( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, -1); +}); diff --git a/netwerk/test/unit/test_http2_with_proxy.js b/netwerk/test/unit/test_http2_with_proxy.js new file mode 100644 index 0000000000..858a0da570 --- /dev/null +++ b/netwerk/test/unit/test_http2_with_proxy.js @@ -0,0 +1,425 @@ +// test HTTP/2 with a HTTP/2 prooxy + +"use strict"; + +/* import-globals-from http2_test_common.js */ +/* import-globals-from head_servers.js */ + +var concurrent_channels = []; + +var loadGroup; +var serverPort; +var proxy; + +add_setup(async function setup() { + serverPort = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(serverPort, null); + dump("using port " + serverPort + "\n"); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. Some older tests in + // this suite use localhost with a TOFU exception, but new ones should use + // foo.example.com + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + Services.prefs.setBoolPref("network.http.http2.enabled", true); + Services.prefs.setBoolPref("network.http.http2.allow-push", true); + Services.prefs.setBoolPref("network.http.altsvc.enabled", true); + Services.prefs.setBoolPref("network.http.altsvc.oe", true); + Services.prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, bar.example.com" + ); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + + Services.prefs.setStringPref( + "services.settings.server", + `data:,#remote-settings-dummy/v1` + ); + + proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); +}); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); + Services.prefs.clearUserPref("network.http.http2.enabled"); + Services.prefs.clearUserPref("network.http.http2.allow-push"); + Services.prefs.clearUserPref("network.http.altsvc.enabled"); + Services.prefs.clearUserPref("network.http.altsvc.oe"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref( + "network.cookieJarSettings.unblocked_for_testing" + ); + + await proxy.stop(); +}); + +// hack - the header test resets the multiplex object on the server, +// so make sure header is always run before the multiplex test. +// +// make sure post_big runs first to test race condition in restarting +// a stalled stream when a SETTINGS frame arrives +add_task(async function do_test_http2_post_big() { + const { httpProxyConnectResponseCode } = await test_http2_post_big( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_basic() { + const { httpProxyConnectResponseCode } = await test_http2_basic(serverPort); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_concurrent() { + const { httpProxyConnectResponseCode } = await test_http2_concurrent( + concurrent_channels, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_concurrent_post() { + const { httpProxyConnectResponseCode } = await test_http2_concurrent_post( + concurrent_channels, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_basic_unblocked_dep() { + const { httpProxyConnectResponseCode } = await test_http2_basic_unblocked_dep( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_nospdy() { + const { httpProxyConnectResponseCode } = await test_http2_nospdy(serverPort); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_push1() { + const { httpProxyConnectResponseCode } = await test_http2_push1( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_push2() { + const { httpProxyConnectResponseCode } = await test_http2_push2( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_push3() { + const { httpProxyConnectResponseCode } = await test_http2_push3( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_push4() { + const { httpProxyConnectResponseCode } = await test_http2_push4( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_push5() { + const { httpProxyConnectResponseCode } = await test_http2_push5( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_push6() { + const { httpProxyConnectResponseCode } = await test_http2_push6( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_doubleheader() { + const { httpProxyConnectResponseCode } = await test_http2_doubleheader( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_xhr() { + await test_http2_xhr(serverPort); +}); + +add_task(async function do_test_http2_header() { + const { httpProxyConnectResponseCode } = await test_http2_header(serverPort); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_invalid_response_header_name_spaces() { + const { httpProxyConnectResponseCode } = + await test_http2_invalid_response_header(serverPort, "name_spaces"); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task( + async function do_test_http2_invalid_response_header_value_line_feed() { + const { httpProxyConnectResponseCode } = + await test_http2_invalid_response_header(serverPort, "value_line_feed"); + Assert.equal(httpProxyConnectResponseCode, 200); + } +); + +add_task( + async function do_test_http2_invalid_response_header_value_carriage_return() { + const { httpProxyConnectResponseCode } = + await test_http2_invalid_response_header( + serverPort, + "value_carriage_return" + ); + Assert.equal(httpProxyConnectResponseCode, 200); + } +); + +add_task(async function do_test_http2_invalid_response_header_value_null() { + const { httpProxyConnectResponseCode } = + await test_http2_invalid_response_header(serverPort, "value_null"); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_cookie_crumbling() { + const { httpProxyConnectResponseCode } = await test_http2_cookie_crumbling( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_multiplex() { + var values = await test_http2_multiplex(serverPort); + Assert.equal(values[0].httpProxyConnectResponseCode, 200); + Assert.equal(values[1].httpProxyConnectResponseCode, 200); + Assert.notEqual(values[0].streamID, values[1].streamID); +}); + +add_task(async function do_test_http2_big() { + const { httpProxyConnectResponseCode } = await test_http2_big(serverPort); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_huge_suspended() { + const { httpProxyConnectResponseCode } = await test_http2_huge_suspended( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_post() { + const { httpProxyConnectResponseCode } = await test_http2_post(serverPort); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_empty_post() { + const { httpProxyConnectResponseCode } = await test_http2_empty_post( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_patch() { + const { httpProxyConnectResponseCode } = await test_http2_patch(serverPort); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_pushapi_1() { + const { httpProxyConnectResponseCode } = await test_http2_pushapi_1( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 0); +}); + +add_task(async function do_test_http2_continuations() { + const { httpProxyConnectResponseCode } = await test_http2_continuations( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 0); +}); + +add_task(async function do_test_http2_blocking_download() { + const { httpProxyConnectResponseCode } = await test_http2_blocking_download( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_illegalhpacksoft() { + const { httpProxyConnectResponseCode } = await test_http2_illegalhpacksoft( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_illegalhpackhard() { + const { httpProxyConnectResponseCode } = await test_http2_illegalhpackhard( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_folded_header() { + const { httpProxyConnectResponseCode } = await test_http2_folded_header( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_empty_data() { + const { httpProxyConnectResponseCode } = await test_http2_empty_data( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_status_phrase() { + const { httpProxyConnectResponseCode } = await test_http2_status_phrase( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_doublepush() { + const { httpProxyConnectResponseCode } = await test_http2_doublepush( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_disk_cache_push() { + const { httpProxyConnectResponseCode } = await test_http2_disk_cache_push( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_h11required_stream() { + // Add new tests above here - best to add new tests before h1 + // streams get too involved + // These next two must always come in this order + const { httpProxyConnectResponseCode } = await test_http2_h11required_stream( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_h11required_session() { + const { httpProxyConnectResponseCode } = await test_http2_h11required_session( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_retry_rst() { + const { httpProxyConnectResponseCode } = await test_http2_retry_rst( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_wrongsuite_tls12() { + // For this test we need to start HTTPS 1.1 proxy because HTTP/2 proxy cannot be used. + proxy.unregisterFilter(); + let proxyHttp1 = new NodeHTTPSProxyServer(); + await proxyHttp1.start(); + proxyHttp1.registerFilter(); + registerCleanupFunction(() => { + proxyHttp1.stop(); + }); + const { httpProxyConnectResponseCode } = await test_http2_wrongsuite_tls12( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); + proxyHttp1.unregisterFilter(); + proxy.registerFilter(); +}); + +add_task(async function do_test_http2_wrongsuite_tls13() { + const { httpProxyConnectResponseCode } = await test_http2_wrongsuite_tls13( + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_push_firstparty1() { + const { httpProxyConnectResponseCode } = await test_http2_push_firstparty1( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_push_firstparty2() { + const { httpProxyConnectResponseCode } = await test_http2_push_firstparty2( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_push_firstparty3() { + const { httpProxyConnectResponseCode } = await test_http2_push_firstparty3( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_push_userContext1() { + const { httpProxyConnectResponseCode } = await test_http2_push_userContext1( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_push_userContext2() { + const { httpProxyConnectResponseCode } = await test_http2_push_userContext2( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); + +add_task(async function do_test_http2_push_userContext3() { + const { httpProxyConnectResponseCode } = await test_http2_push_userContext3( + loadGroup, + serverPort + ); + Assert.equal(httpProxyConnectResponseCode, 200); +}); diff --git a/netwerk/test/unit/test_http3.js b/netwerk/test/unit/test_http3.js new file mode 100644 index 0000000000..4fd4c0be22 --- /dev/null +++ b/netwerk/test/unit/test_http3.js @@ -0,0 +1,569 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// Generate a post with known pre-calculated md5 sum. +function generateContent(size) { + let content = ""; + for (let i = 0; i < size; i++) { + content += "0"; + } + return content; +} + +let post = generateContent(10); + +// Max concurent stream number in neqo is 100. +// Openning 120 streams will test queuing of streams. +let number_of_parallel_requests = 120; +let h1Server = null; +let h3Route; +let httpsOrigin; +let httpOrigin; +let h3AltSvc; + +let prefs; + +let tests = [ + // This test must be the first because it setsup alt-svc connection, that + // other tests use. + test_https_alt_svc, + test_multiple_requests, + test_request_cancelled_by_server, + test_stream_cancelled_by_necko, + test_multiple_request_one_is_cancelled, + test_multiple_request_one_is_cancelled_by_necko, + test_post, + test_patch, + test_http_alt_svc, + test_slow_receiver, + // This test should be at the end, because it will close http3 + // connection and the transaction will switch to already existing http2 + // connection. + // TODO: Bug 1582667 should try to fix issue with connection being closed. + test_version_fallback, + testsDone, +]; + +let current_test = 0; + +function run_next_test() { + if (current_test < tests.length) { + dump("starting test number " + current_test + "\n"); + tests[current_test](); + current_test++; + } +} + +function run_test() { + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + let h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + h3AltSvc = ":" + h3Port; + + h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + prefs = Services.prefs; + + prefs.setBoolPref("network.http.http3.enable", true); + prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + // We always resolve elements of localDomains as it's hardcoded without the + // following pref: + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + prefs.setBoolPref("network.http.altsvc.oe", true); + + // The certificate for the http3server server is for foo.example.com and + // is signed by http2-ca.pem so add that cert to the trust list as a + // signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + httpsOrigin = "https://foo.example.com:" + h2Port + "/"; + + h1Server = new HttpServer(); + h1Server.registerPathHandler("/http3-test", h1Response); + h1Server.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK); + h1Server.registerPathHandler("/VersionFallback", h1Response); + h1Server.start(-1); + h1Server.identity.setPrimary( + "http", + "foo.example.com", + h1Server.identity.primaryPort + ); + httpOrigin = "http://foo.example.com:" + h1Server.identity.primaryPort + "/"; + + run_next_test(); +} + +function h1Response(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + try { + let hval = "h3-29=" + metadata.getHeader("x-altsvc"); + response.setHeader("Alt-Svc", hval, false); + } catch (e) {} + + let body = "Q: What did 0 say to 8? A: Nice Belt!\n"; + response.bodyOutputStream.write(body, body.length); +} + +function h1ServerWK(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + let body = '["http://foo.example.com:' + h1Server.identity.primaryPort + '"]'; + response.bodyOutputStream.write(body, body.length); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let Http3CheckListener = function () {}; + +Http3CheckListener.prototype = { + onDataAvailableFired: false, + expectedStatus: Cr.NS_OK, + expectedRoute: "", + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + Assert.equal(request.status, this.expectedStatus); + if (Components.isSuccessCode(this.expectedStatus)) { + Assert.equal(request.responseStatus, 200); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, this.expectedStatus); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, this.expectedRoute); + + if (Components.isSuccessCode(this.expectedStatus)) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + Assert.equal(this.onDataAvailableFired, true); + Assert.equal(request.getResponseHeader("X-Firefox-Http3"), "h3-29"); + } + run_next_test(); + do_test_finished(); + }, +}; + +let WaitForHttp3Listener = function () {}; + +WaitForHttp3Listener.prototype = new Http3CheckListener(); + +WaitForHttp3Listener.prototype.uri = ""; +WaitForHttp3Listener.prototype.h3AltSvc = ""; + +WaitForHttp3Listener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + Assert.equal(status, this.expectedStatus); + + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + + if (routed == this.expectedRoute) { + Assert.equal(routed, this.expectedRoute); // always true, but a useful log + Assert.equal(httpVersion, "h3-29"); + run_next_test(); + } else { + dump("poll later for alt svc mapping\n"); + if (httpVersion == "h2") { + request.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.ok(request.supportsHTTP3); + } + do_test_pending(); + do_timeout(500, () => { + doTest(this.uri, this.expectedRoute, this.h3AltSvc); + }); + } + + do_test_finished(); +}; + +function doTest(uri, expectedRoute, altSvc) { + let chan = makeChan(uri); + let listener = new WaitForHttp3Listener(); + listener.uri = uri; + listener.expectedRoute = expectedRoute; + listener.h3AltSvc = altSvc; + chan.setRequestHeader("x-altsvc", altSvc, false); + chan.asyncOpen(listener); +} + +// Test Alt-Svc for http3. +// H2 server returns alt-svc=h3-29=:h3port +function test_https_alt_svc() { + dump("test_https_alt_svc()\n"); + do_test_pending(); + doTest(httpsOrigin + "http3-test", h3Route, h3AltSvc); +} + +// Listener for a number of parallel requests. if with_error is set, one of +// the channels will be cancelled (by the server or in onStartRequest). +let MultipleListener = function () {}; + +MultipleListener.prototype = { + number_of_parallel_requests: 0, + with_error: Cr.NS_OK, + count_of_done_requests: 0, + error_found_onstart: false, + error_found_onstop: false, + need_cancel_found: false, + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + let need_cancel = ""; + try { + need_cancel = request.getRequestHeader("CancelMe"); + } catch (e) {} + if (need_cancel != "") { + this.need_cancel_found = true; + request.cancel(Cr.NS_ERROR_ABORT); + } else if (Components.isSuccessCode(request.status)) { + Assert.equal(request.responseStatus, 200); + } else if (this.error_found_onstart) { + do_throw("We should have only one request failing."); + } else { + Assert.equal(request.status, this.with_error); + this.error_found_onstart = true; + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let routed = ""; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + Assert.equal(routed, this.expectedRoute); + + if (Components.isSuccessCode(request.status)) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + } + + if (!Components.isSuccessCode(request.status)) { + if (this.error_found_onstop) { + do_throw("We should have only one request failing."); + } else { + Assert.equal(request.status, this.with_error); + this.error_found_onstop = true; + } + } + this.count_of_done_requests++; + if (this.count_of_done_requests == this.number_of_parallel_requests) { + if (Components.isSuccessCode(this.with_error)) { + Assert.equal(this.error_found_onstart, false); + Assert.equal(this.error_found_onstop, false); + } else { + Assert.ok(this.error_found_onstart || this.need_cancel_found); + Assert.equal(this.error_found_onstop, true); + } + run_next_test(); + } + do_test_finished(); + }, +}; + +// Multiple requests +function test_multiple_requests() { + dump("test_multiple_requests()\n"); + + let listener = new MultipleListener(); + listener.number_of_parallel_requests = number_of_parallel_requests; + listener.expectedRoute = h3Route; + + for (let i = 0; i < number_of_parallel_requests; i++) { + let chan = makeChan(httpsOrigin + "20000"); + chan.asyncOpen(listener); + do_test_pending(); + } +} + +// A request cancelled by a server. +function test_request_cancelled_by_server() { + dump("test_request_cancelled_by_server()\n"); + + let listener = new Http3CheckListener(); + listener.expectedStatus = Cr.NS_ERROR_NET_INTERRUPT; + listener.expectedRoute = h3Route; + let chan = makeChan(httpsOrigin + "RequestCancelled"); + chan.asyncOpen(listener); + do_test_pending(); +} + +let CancelRequestListener = function () {}; + +CancelRequestListener.prototype = new Http3CheckListener(); + +CancelRequestListener.prototype.expectedStatus = Cr.NS_ERROR_ABORT; + +CancelRequestListener.prototype.onStartRequest = function testOnStartRequest( + request +) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + Assert.equal(Components.isSuccessCode(request.status), true); + request.cancel(Cr.NS_ERROR_ABORT); +}; + +// Cancel stream after OnStartRequest. +function test_stream_cancelled_by_necko() { + dump("test_stream_cancelled_by_necko()\n"); + + let listener = new CancelRequestListener(); + listener.expectedRoute = h3Route; + let chan = makeChan(httpsOrigin + "20000"); + chan.asyncOpen(listener); + do_test_pending(); +} + +// Multiple requests, one gets cancelled by the server, the other should finish normally. +function test_multiple_request_one_is_cancelled() { + dump("test_multiple_request_one_is_cancelled()\n"); + + let listener = new MultipleListener(); + listener.number_of_parallel_requests = number_of_parallel_requests; + listener.with_error = Cr.NS_ERROR_NET_INTERRUPT; + listener.expectedRoute = h3Route; + + for (let i = 0; i < number_of_parallel_requests; i++) { + let uri = httpsOrigin + "20000"; + if (i == 4) { + // Add a request that will be cancelled by the server. + uri = httpsOrigin + "RequestCancelled"; + } + let chan = makeChan(uri); + chan.asyncOpen(listener); + do_test_pending(); + } +} + +// Multiple requests, one gets cancelled by us, the other should finish normally. +function test_multiple_request_one_is_cancelled_by_necko() { + dump("test_multiple_request_one_is_cancelled_by_necko()\n"); + + let listener = new MultipleListener(); + listener.number_of_parallel_requests = number_of_parallel_requests; + listener.with_error = Cr.NS_ERROR_ABORT; + listener.expectedRoute = h3Route; + for (let i = 0; i < number_of_parallel_requests; i++) { + let chan = makeChan(httpsOrigin + "20000"); + if (i == 4) { + // MultipleListener will cancel request with this header. + chan.setRequestHeader("CancelMe", "true", false); + } + chan.asyncOpen(listener); + do_test_pending(); + } +} + +let PostListener = function () {}; + +PostListener.prototype = new Http3CheckListener(); + +PostListener.prototype.onDataAvailable = function (request, stream, off, cnt) { + this.onDataAvailableFired = true; + read_stream(stream, cnt); +}; + +// Support for doing a POST +function do_post(content, chan, listener, method) { + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = content; + + let uchan = chan.QueryInterface(Ci.nsIUploadChannel); + uchan.setUploadStream(stream, "text/plain", stream.available()); + + chan.requestMethod = method; + + chan.asyncOpen(listener); +} + +// Test a simple POST +function test_post() { + dump("test_post()"); + let chan = makeChan(httpsOrigin + "post"); + let listener = new PostListener(); + listener.expectedRoute = h3Route; + do_post(post, chan, listener, "POST"); + do_test_pending(); +} + +// Test a simple PATCH +function test_patch() { + dump("test_patch()"); + let chan = makeChan(httpsOrigin + "patch"); + let listener = new PostListener(); + listener.expectedRoute = h3Route; + do_post(post, chan, listener, "PATCH"); + do_test_pending(); +} + +// Test alt-svc for http (without s) +function test_http_alt_svc() { + dump("test_http_alt_svc()\n"); + + do_test_pending(); + doTest(httpOrigin + "http3-test", h3Route, h3AltSvc); +} + +let SlowReceiverListener = function () {}; + +SlowReceiverListener.prototype = new Http3CheckListener(); +SlowReceiverListener.prototype.count = 0; + +SlowReceiverListener.prototype.onDataAvailable = function ( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.count += cnt; + read_stream(stream, cnt); +}; + +SlowReceiverListener.prototype.onStopRequest = function (request, status) { + Assert.equal(status, this.expectedStatus); + Assert.equal(this.count, 10000000); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, this.expectedRoute); + + if (Components.isSuccessCode(this.expectedStatus)) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + Assert.equal(this.onDataAvailableFired, true); + } + run_next_test(); + do_test_finished(); +}; + +function test_slow_receiver() { + dump("test_slow_receiver()\n"); + let chan = makeChan(httpsOrigin + "10000000"); + let listener = new SlowReceiverListener(); + listener.expectedRoute = h3Route; + chan.asyncOpen(listener); + do_test_pending(); + chan.suspend(); + do_timeout(1000, chan.resume); +} + +let CheckFallbackListener = function () {}; + +CheckFallbackListener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, "0"); + + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "http/1.1"); + run_next_test(); + do_test_finished(); + }, +}; + +// Server cancels request with VersionFallback. +function test_version_fallback() { + dump("test_version_fallback()\n"); + + let chan = makeChan(httpsOrigin + "VersionFallback"); + let listener = new CheckFallbackListener(); + chan.asyncOpen(listener); + do_test_pending(); +} + +function testsDone() { + prefs.clearUserPref("network.http.http3.enable"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + prefs.clearUserPref("network.http.altsvc.oe"); + dump("testDone\n"); + do_test_pending(); + h1Server.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_http3_0rtt.js b/netwerk/test/unit/test_http3_0rtt.js new file mode 100644 index 0000000000..1002263ed5 --- /dev/null +++ b/netwerk/test/unit/test_http3_0rtt.js @@ -0,0 +1,96 @@ +/* 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/. */ + +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +registerCleanupFunction(async () => { + http3_clear_prefs(); +}); + +add_task(async function setup() { + await http3_setup_tests("h3-29"); +}); + +let Http3Listener = function () {}; + +Http3Listener.prototype = { + resumed: false, + + onStartRequest: function testOnStartRequest(request) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(request.responseStatus, 200); + + let secinfo = request.securityInfo; + Assert.equal(secinfo.resumed, this.resumed); + Assert.ok(secinfo.serverCert != null); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + + this.finish(); + }, +}; + +function chanPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +async function test_first_conn_no_resumed() { + let listener = new Http3Listener(); + listener.resumed = false; + let chan = makeChan("https://foo.example.com/30"); + await chanPromise(chan, listener); +} + +async function test_0RTT(enable_0rtt, resumed) { + info(`enable_0rtt=${enable_0rtt} resumed=${resumed}`); + Services.prefs.setBoolPref("network.http.http3.enable_0rtt", enable_0rtt); + + // Make sure the h3 connection created by the previous test is cleared. + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // This connecion should be resumed. + let listener = new Http3Listener(); + listener.resumed = resumed; + let chan = makeChan("https://foo.example.com/30"); + await chanPromise(chan, listener); +} + +add_task(async function test_0RTT_setups() { + await test_first_conn_no_resumed(); + + // http3.0RTT enabled + await test_0RTT(true, true); + + // http3.0RTT disabled + await test_0RTT(false, false); +}); diff --git a/netwerk/test/unit/test_http3_421.js b/netwerk/test/unit/test_http3_421.js new file mode 100644 index 0000000000..de04babe06 --- /dev/null +++ b/netwerk/test/unit/test_http3_421.js @@ -0,0 +1,172 @@ +"use strict"; + +let h3Route; +let httpsOrigin; +let h3AltSvc; +let prefs; + +let tests = [test_https_alt_svc, test_response_421, testsDone]; +let current_test = 0; + +function run_next_test() { + if (current_test < tests.length) { + dump("starting test number " + current_test + "\n"); + tests[current_test](); + current_test++; + } +} + +function run_test() { + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + let h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + h3AltSvc = ":" + h3Port; + + h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + prefs = Services.prefs; + + prefs.setBoolPref("network.http.http3.enable", true); + prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + // We always resolve elements of localDomains as it's hardcoded without the + // following pref: + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + + // The certificate for the http3server server is for foo.example.com and + // is signed by http2-ca.pem so add that cert to the trust list as a + // signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + httpsOrigin = "https://foo.example.com:" + h2Port + "/"; + + run_next_test(); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let Http3Listener = function () {}; + +Http3Listener.prototype = { + onDataAvailableFired: false, + buffer: "", + routed: "", + httpVersion: "", + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + this.buffer = this.buffer.concat(read_stream(stream, cnt)); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + Assert.equal(this.onDataAvailableFired, true); + this.routed = "NA"; + try { + this.routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + this.routed + "\n"); + + this.httpVersion = ""; + try { + this.httpVersion = request.protocolVersion; + } catch (e) {} + dump("httpVersion is " + this.httpVersion + "\n"); + }, +}; + +let WaitForHttp3Listener = function () {}; + +WaitForHttp3Listener.prototype = new Http3Listener(); + +WaitForHttp3Listener.prototype.uri = ""; + +WaitForHttp3Listener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + Http3Listener.prototype.onStopRequest.call(this, request, status); + + if (this.routed == h3Route) { + Assert.equal(this.httpVersion, "h3-29"); + run_next_test(); + } else { + dump("poll later for alt svc mapping\n"); + do_test_pending(); + do_timeout(500, () => { + doTest(this.uri); + }); + } + + do_test_finished(); +}; + +function doTest(uri) { + let chan = makeChan(uri); + let listener = new WaitForHttp3Listener(); + listener.uri = uri; + chan.setRequestHeader("x-altsvc", h3AltSvc, false); + chan.asyncOpen(listener); +} + +// Test Alt-Svc for http3. +// H2 server returns alt-svc=h3-29=:h3port +function test_https_alt_svc() { + dump("test_https_alt_svc()\n"); + + do_test_pending(); + doTest(httpsOrigin + "http3-test"); +} + +let Resp421Listener = function () {}; + +Resp421Listener.prototype = new Http3Listener(); + +Resp421Listener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + Http3Listener.prototype.onStopRequest.call(this, request, status); + + Assert.equal(this.routed, "0"); + Assert.equal(this.httpVersion, "h2"); + Assert.ok(this.buffer.match("You Win! [(]by requesting/Response421[)]")); + + run_next_test(); + do_test_finished(); +}; + +function test_response_421() { + dump("test_response_421()\n"); + + let listener = new Resp421Listener(); + let chan = makeChan(httpsOrigin + "Response421"); + chan.asyncOpen(listener); + do_test_pending(); +} + +function testsDone() { + prefs.clearUserPref("network.http.http3.enable"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + dump("testDone\n"); + do_test_pending(); + do_test_finished(); +} diff --git a/netwerk/test/unit/test_http3_alt_svc.js b/netwerk/test/unit/test_http3_alt_svc.js new file mode 100644 index 0000000000..201101eb19 --- /dev/null +++ b/netwerk/test/unit/test_http3_alt_svc.js @@ -0,0 +1,136 @@ +"use strict"; + +let httpsOrigin; +let h3AltSvc; +let h3Route; +let prefs; + +let tests = [test_https_alt_svc, testsDone]; + +let current_test = 0; + +function run_next_test() { + if (current_test < tests.length) { + dump("starting test number " + current_test + "\n"); + tests[current_test](); + current_test++; + } +} + +function run_test() { + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + let h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + h3AltSvc = ":" + h3Port; + + h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + prefs = Services.prefs; + + prefs.setBoolPref("network.http.http3.enable", true); + prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + // We always resolve elements of localDomains as it's hardcoded without the + // following pref: + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + + // The certificate for the http3server server is for foo.example.com and + // is signed by http2-ca.pem so add that cert to the trust list as a + // signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + httpsOrigin = "https://foo.example.com:" + h2Port + "/"; + + run_next_test(); +} + +function createPrincipal(url) { + var ssm = Services.scriptSecurityManager; + try { + return ssm.createContentPrincipal(Services.io.newURI(url), {}); + } catch (e) { + return null; + } +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadingPrincipal: createPrincipal(uri), + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let WaitForHttp3Listener = function () {}; + +WaitForHttp3Listener.prototype = { + onDataAvailableFired: false, + expectedRoute: "", + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + if (routed == this.expectedRoute) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + run_next_test(); + } else { + dump("poll later for alt svc mapping\n"); + do_test_pending(); + do_timeout(500, () => { + doTest(this.uri, this.expectedRoute, this.h3AltSvc); + }); + } + + do_test_finished(); + }, +}; + +function doTest(uri, expectedRoute, altSvc) { + let chan = makeChan(uri); + let listener = new WaitForHttp3Listener(); + listener.uri = uri; + listener.expectedRoute = expectedRoute; + listener.h3AltSvc = altSvc; + chan.setRequestHeader("x-altsvc", altSvc, false); + chan.asyncOpen(listener); +} + +// Test Alt-Svc for http3. +// H2 server returns alt-svc=h2=foo2.example.com:8000,h3-29=:h3port,h3-30=foo2.example.com:8443 +function test_https_alt_svc() { + dump("test_https_alt_svc()\n"); + do_test_pending(); + doTest(httpsOrigin + "http3-test2", h3Route, h3AltSvc); +} + +function testsDone() { + prefs.clearUserPref("network.http.http3.enable"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + dump("testDone\n"); +} diff --git a/netwerk/test/unit/test_http3_coalescing.js b/netwerk/test/unit/test_http3_coalescing.js new file mode 100644 index 0000000000..b6c19ef626 --- /dev/null +++ b/netwerk/test/unit/test_http3_coalescing.js @@ -0,0 +1,113 @@ +/* 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"; + +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +let h2Port; +let h3Port; + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +function setup() { + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + Services.prefs.setBoolPref("network.http.http3.enable", true); +} + +setup(); +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + Services.prefs.clearUserPref("network.dns.echconfig.enabled"); + Services.prefs.clearUserPref( + "network.dns.echconfig.fallback_to_origin_when_all_failed" + ); + Services.prefs.clearUserPref("network.dns.httpssvc.reset_exclustion_list"); + Services.prefs.clearUserPref("network.http.http3.enable"); + Services.prefs.clearUserPref( + "network.dns.httpssvc.http3_fast_fallback_timeout" + ); + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + } + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +async function H3CoalescingTest(host1, host2) { + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + `${host1};h3-29=:${h3Port}` + ); + Services.prefs.setCharPref("network.dns.localDomains", host1); + + let chan = makeChan(`https://${host1}`); + let [req] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.protocolVersion, "h3-29"); + let hash = req.getResponseHeader("x-http3-conn-hash"); + + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + `${host2};h3-29=:${h3Port}` + ); + Services.prefs.setCharPref("network.dns.localDomains", host2); + + chan = makeChan(`https://${host2}`); + [req] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.protocolVersion, "h3-29"); + // The port used by the second connection should be the same as the first one. + Assert.equal(req.getResponseHeader("x-http3-conn-hash"), hash); +} + +add_task(async function testH3CoalescingWithSpeculativeConnection() { + await http3_setup_tests("h3-29"); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + await H3CoalescingTest("foo.h3_coalescing.org", "bar.h3_coalescing.org"); +}); + +add_task(async function testH3CoalescingWithoutSpeculativeConnection() { + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + await H3CoalescingTest("baz.h3_coalescing.org", "qux.h3_coalescing.org"); +}); diff --git a/netwerk/test/unit/test_http3_direct_proxy.js b/netwerk/test/unit/test_http3_direct_proxy.js new file mode 100644 index 0000000000..74d2b11fc1 --- /dev/null +++ b/netwerk/test/unit/test_http3_direct_proxy.js @@ -0,0 +1,54 @@ +/* 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/. */ + +// Test if a HTTP3 connection can be established when a proxy info says +// to use direct connection + +"use strict"; + +registerCleanupFunction(async () => { + http3_clear_prefs(); + Services.prefs.clearUserPref("network.proxy.type"); + Services.prefs.clearUserPref("network.proxy.autoconfig_url"); +}); + +add_task(async function setup() { + await http3_setup_tests("h3-29"); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +add_task(async function testHttp3WithDirectProxy() { + var pac = + "data:text/plain," + + "function FindProxyForURL(url, host) {" + + ' return "DIRECT; PROXY foopy:8080;"' + + "}"; + + // Configure PAC + Services.prefs.setIntPref("network.proxy.type", 2); + Services.prefs.setCharPref("network.proxy.autoconfig_url", pac); + + let chan = makeChan(`https://foo.example.com`); + let [req] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.protocolVersion, "h3-29"); +}); diff --git a/netwerk/test/unit/test_http3_dns_retry.js b/netwerk/test/unit/test_http3_dns_retry.js new file mode 100644 index 0000000000..82c1eb3f81 --- /dev/null +++ b/netwerk/test/unit/test_http3_dns_retry.js @@ -0,0 +1,283 @@ +/* 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"; + +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +let h2Port; +let h3Port; +let trrServer; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +add_setup(async function setup() { + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + trr_test_setup(); + + if (mozinfo.socketprocess_networking) { + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } + + Services.prefs.setIntPref("network.trr.mode", 2); // TRR first + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + Services.prefs.setBoolPref( + "network.http.http3.block_loopback_ipv6_addr", + true + ); + + registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.http.http3.block_loopback_ipv6_addr"); + if (trrServer) { + await trrServer.stop(); + } + }); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function channelOpenPromise(chan, flags) { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + } + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +async function registerDoHAnswers(host, ipv4Answers, ipv6Answers, httpsRecord) { + trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers(host, "HTTPS", { + answers: httpsRecord, + }); + + await trrServer.registerDoHAnswers(host, "AAAA", { + answers: ipv6Answers, + }); + + await trrServer.registerDoHAnswers(host, "A", { + answers: ipv4Answers, + }); + + Services.dns.clearCache(true); +} + +// Test if we retry IPv4 address for Http/3 properly. +add_task(async function test_retry_with_ipv4() { + let host = "test.http3_retry.com"; + let ipv4answers = [ + { + name: host, + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]; + // The UDP socket will return connection refused error because we set + // "network.http.http3.block_loopback_ipv6_addr" to true. + let ipv6answers = [ + { + name: host, + ttl: 55, + type: "AAAA", + flush: false, + data: "::1", + }, + ]; + let httpsRecord = [ + { + name: host, + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: host, + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + ], + }, + }, + ]; + + await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord); + + let chan = makeChan(`https://${host}`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h3-29"); + + await trrServer.stop(); +}); + +add_task(async function test_retry_with_ipv4_disabled() { + let host = "test.http3_retry_ipv4_blocked.com"; + let ipv4answers = [ + { + name: host, + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]; + // The UDP socket will return connection refused error because we set + // "network.http.http3.block_loopback_ipv6_addr" to true. + let ipv6answers = [ + { + name: host, + ttl: 55, + type: "AAAA", + flush: false, + data: "::1", + }, + ]; + let httpsRecord = [ + { + name: host, + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: host, + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + ], + }, + }, + ]; + + await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord); + + let chan = makeChan(`https://${host}`); + chan.QueryInterface(Ci.nsIHttpChannelInternal); + chan.setIPv4Disabled(); + + await channelOpenPromise(chan, CL_EXPECT_FAILURE); + await trrServer.stop(); +}); + +// See bug 1837252. There is no way to observe the outcome of this test, because +// the crash in bug 1837252 is only triggered by speculative connection. +// The outcome of this test is no crash. +add_task(async function test_retry_with_ipv4_failed() { + let host = "test.http3_retry_failed.com"; + // Return a wrong answer intentionally. + let ipv4answers = [ + { + name: host, + ttl: 55, + type: "AAAA", + flush: false, + data: "127.0.0.1", + }, + ]; + // The UDP socket will return connection refused error because we set + // "network.http.http3.block_loopback_ipv6_addr" to true. + let ipv6answers = [ + { + name: host, + ttl: 55, + type: "AAAA", + flush: false, + data: "::1", + }, + ]; + let httpsRecord = [ + { + name: host, + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: host, + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + ], + }, + }, + ]; + + await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord); + + // This speculative connection is used to trigger the mechanism to retry + // Http/3 connection with a IPv4 address. + // We want to make the connection entry's IP preference known, + // so DnsAndConnectSocket::mRetryWithDifferentIPFamily will be set to true + // before the second speculative connection. + let uri = Services.io.newURI(`https://test.http3_retry_failed.com`); + Services.io.speculativeConnect( + uri, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + false + ); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 3000)); + + // When this speculative connection is created, the connection entry is + // already set to prefer IPv4. Since we provided an invalid A response, + // DnsAndConnectSocket::OnLookupComplete is called with an error. + // Since DnsAndConnectSocket::mRetryWithDifferentIPFamily is true, we do + // retry DNS lookup. During retry, we should not create UDP connection. + Services.io.speculativeConnect( + uri, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + false + ); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 3000)); + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_http3_early_hint_listener.js b/netwerk/test/unit/test_http3_early_hint_listener.js new file mode 100644 index 0000000000..5100ea3151 --- /dev/null +++ b/netwerk/test/unit/test_http3_early_hint_listener.js @@ -0,0 +1,92 @@ +/* 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"; + +// The server will always respond with a 103 EarlyHint followed by a +// 200 response. +// 103 response contains: +// 1) a non-link header +// 2) a link header if a request has a "link-to-set" header. If the +// request header is not set, the response will not have Link headers. +// A "link-to-set" header may contain multiple link headers +// separated with a comma. + +const earlyhintspath = "/103_response"; +const hint1 = "</style.css>; rel=preload; as=style"; +const hint2 = "</img.png>; rel=preload; as=image"; + +let EarlyHintsListener = function () {}; + +EarlyHintsListener.prototype = { + _expected_hints: [], + earlyHintsReceived: 0, + + QueryInterface: ChromeUtils.generateQI(["nsIEarlyHintObserver"]), + + earlyHint(header) { + Assert.ok(this._expected_hints.includes(header)); + this.earlyHintsReceived += 1; + }, +}; + +function chanPromise(uri, listener, headers) { + var principal = Services.scriptSecurityManager.createContentPrincipal( + NetUtil.newURI(uri), + {} + ); + var chan = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + chan + .QueryInterface(Ci.nsIHttpChannel) + .setRequestHeader("link-to-set", headers, false); + chan.QueryInterface(Ci.nsIHttpChannelInternal).setEarlyHintObserver(listener); + + return promiseAsyncOpen(chan); +} + +registerCleanupFunction(async () => { + http3_clear_prefs(); +}); + +add_task(async function setup() { + await http3_setup_tests("h3-29"); +}); + +add_task(async function early_hints() { + let earlyHints = new EarlyHintsListener(); + earlyHints._expected_hints = [hint1]; + + await chanPromise( + `https://foo.example.com${earlyhintspath}`, + earlyHints, + hint1 + ); + Assert.equal(earlyHints.earlyHintsReceived, 1); +}); + +// Test when there is no Link header in a 103 response. +// 103 response will contain non-link headers. +add_task(async function no_early_hints() { + let earlyHints = new EarlyHintsListener(); + + await chanPromise(`https://foo.example.com${earlyhintspath}`, earlyHints, ""); + Assert.equal(earlyHints.earlyHintsReceived, 0); +}); + +add_task(async function early_hints_multiple() { + let earlyHints = new EarlyHintsListener(); + earlyHints._expected_hints = [hint1, hint2]; + + await chanPromise( + `https://foo.example.com${earlyhintspath}`, + earlyHints, + hint1 + ", " + hint2 + ); + Assert.equal(earlyHints.earlyHintsReceived, 2); +}); diff --git a/netwerk/test/unit/test_http3_error_before_connect.js b/netwerk/test/unit/test_http3_error_before_connect.js new file mode 100644 index 0000000000..72f8d61182 --- /dev/null +++ b/netwerk/test/unit/test_http3_error_before_connect.js @@ -0,0 +1,109 @@ +/* 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"; + +let httpsUri; + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.http3.enable"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.dns.disableIPv6"); + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); + Services.prefs.clearUserPref("network.http.http3.backup_timer_delay"); + dump("cleanup done\n"); +}); + +function makeChan() { + let chan = NetUtil.newChannel({ + uri: httpsUri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +add_task(async function test_setup() { + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + let h3Port = Services.env.get("MOZHTTP3_PORT_NO_RESPONSE"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + // Set AltSvc to point to not existing HTTP3 server on port 443 + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "foo.example.com;h3-29=:" + h3Port + ); + Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 0); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + httpsUri = "https://foo.example.com:" + h2Port + "/"; +}); + +add_task(async function test_fatal_stream_error() { + let result = 1; + // We need to loop here because we need to wait for AltSvc storage to + // to be started. + // We also do not have a way to verify that HTTP3 has been tried, because + // the fallback is automatic, so try a couple of times. + do { + // We need to close HTTP2 connections, otherwise our connection pooling + // will dispatch the request over already existing HTTP2 connection. + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); + result++; + } while (result < 5); +}); + +let CheckOnlyHttp2Listener = function () {}; + +CheckOnlyHttp2Listener.prototype = { + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h2"); + + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + Assert.ok(routed === "0" || routed === "NA"); + this.finish(); + }, +}; + +add_task(async function test_no_http3_after_error() { + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); +}); + +// also after all connections are closed. +add_task(async function test_no_http3_after_error2() { + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); +}); diff --git a/netwerk/test/unit/test_http3_fast_fallback.js b/netwerk/test/unit/test_http3_fast_fallback.js new file mode 100644 index 0000000000..4b32b8a2b9 --- /dev/null +++ b/netwerk/test/unit/test_http3_fast_fallback.js @@ -0,0 +1,908 @@ +/* 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"; + +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +let h2Port; +let h3Port; +let trrServer; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +add_setup(async function setup() { + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + h3Port = Services.env.get("MOZHTTP3_PORT_NO_RESPONSE"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + trr_test_setup(); + + if (mozinfo.socketprocess_networking) { + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } + + Services.prefs.setIntPref("network.trr.mode", 2); // TRR first + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + + registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + Services.prefs.clearUserPref("network.dns.echconfig.enabled"); + Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled"); + Services.prefs.clearUserPref( + "network.dns.echconfig.fallback_to_origin_when_all_failed" + ); + Services.prefs.clearUserPref("network.dns.httpssvc.reset_exclustion_list"); + Services.prefs.clearUserPref("network.http.http3.enable"); + Services.prefs.clearUserPref( + "network.dns.httpssvc.http3_fast_fallback_timeout" + ); + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); + Services.prefs.clearUserPref("network.http.http3.backup_timer_delay"); + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); + Services.prefs.clearUserPref( + "network.http.http3.parallel_fallback_conn_limit" + ); + if (trrServer) { + await trrServer.stop(); + } + }); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function channelOpenPromise(chan, flags, delay) { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + } + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + if (delay) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, delay)); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +let CheckOnlyHttp2Listener = function () {}; + +CheckOnlyHttp2Listener.prototype = { + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h2"); + + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + Assert.ok(routed === "0" || routed === "NA"); + this.finish(); + }, +}; + +async function fast_fallback_test() { + let result = 1; + // We need to loop here because we need to wait for AltSvc storage to + // to be started. + // We also do not have a way to verify that HTTP3 has been tried, because + // the fallback is automatic, so try a couple of times. + do { + // We need to close HTTP2 connections, otherwise our connection pooling + // will dispatch the request over already existing HTTP2 connection. + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(`https://foo.example.com:${h2Port}/`); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); + result++; + } while (result < 3); +} + +// Test the case when speculative connection is enabled. In this case, when the +// backup connection is ready, the http transaction is still in pending +// queue because the h3 connection is never ready to accept transactions. +add_task(async function test_fast_fallback_with_speculative_connection() { + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + // Set AltSvc to point to not existing HTTP3 server on port 443 + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "foo.example.com;h3-29=:" + h3Port + ); + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + + await fast_fallback_test(); +}); + +// Test the case when speculative connection is disabled. In this case, when the +// back connection is ready, the http transaction is already activated, +// but the socket is not ready to write. +add_task(async function test_fast_fallback_without_speculative_connection() { + // Make sure the h3 connection created by the previous test is cleared. + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + // Clear the h3 excluded list, otherwise the Alt-Svc mapping will not be used. + Services.obs.notifyObservers(null, "network:reset-http3-excluded-list"); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + + await fast_fallback_test(); + + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); +}); + +// Test when echConfig is disabled and we have https rr for http3. We use a +// longer timeout in this test, so when fast fallback timer is triggered, the +// http transaction is already activated. +add_task(async function testFastfallback() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", false); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 1000 + ); + + await trrServer.registerDoHAnswers("test.fastfallback.com", "HTTPS", { + answers: [ + { + name: "test.fastfallback.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.fastfallback1.com", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.fastfallback.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.fastfallback2.com", + values: [ + { key: "alpn", value: "h2" }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("test.fastfallback1.com", "A", { + answers: [ + { + name: "test.fastfallback1.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await trrServer.registerDoHAnswers("test.fastfallback2.com", "A", { + answers: [ + { + name: "test.fastfallback2.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + let chan = makeChan(`https://test.fastfallback.com/server-timing`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + + await trrServer.stop(); +}); + +// Like the previous test, but with a shorter timeout, so when fast fallback +// timer is triggered, the http transaction is still in pending queue. +add_task(async function testFastfallback1() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", false); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 10 + ); + + await trrServer.registerDoHAnswers("test.fastfallback.org", "HTTPS", { + answers: [ + { + name: "test.fastfallback.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.fastfallback1.org", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.fastfallback.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.fastfallback2.org", + values: [ + { key: "alpn", value: "h2" }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("test.fastfallback1.org", "A", { + answers: [ + { + name: "test.fastfallback1.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await trrServer.registerDoHAnswers("test.fastfallback2.org", "A", { + answers: [ + { + name: "test.fastfallback2.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + let chan = makeChan(`https://test.fastfallback.org/server-timing`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + + await trrServer.stop(); +}); + +// Test when echConfig is enabled, we can sucessfully fallback to the last +// record. +add_task(async function testFastfallbackWithEchConfig() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 50 + ); + + await trrServer.registerDoHAnswers("test.ech.org", "HTTPS", { + answers: [ + { + name: "test.ech.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.ech1.org", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.ech.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.ech2.org", + values: [ + { key: "alpn", value: "h2" }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.ech.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 3, + name: "test.ech3.org", + values: [ + { key: "alpn", value: "h2" }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("test.ech1.org", "A", { + answers: [ + { + name: "test.ech1.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await trrServer.registerDoHAnswers("test.ech3.org", "A", { + answers: [ + { + name: "test.ech3.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + let chan = makeChan(`https://test.ech.org/server-timing`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + + await trrServer.stop(); +}); + +// Test when echConfig is enabled, the connection should fail when not all +// records have echConfig. +add_task(async function testFastfallbackWithpartialEchConfig() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 50 + ); + + await trrServer.registerDoHAnswers("test.partial_ech.org", "HTTPS", { + answers: [ + { + name: "test.partial_ech.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.partial_ech1.org", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.partial_ech.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.partial_ech2.org", + values: [ + { key: "alpn", value: "h2" }, + { key: "port", value: h2Port }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("test.partial_ech1.org", "A", { + answers: [ + { + name: "test.partial_ech1.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await trrServer.registerDoHAnswers("test.partial_ech2.org", "A", { + answers: [ + { + name: "test.partial_ech2.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + let chan = makeChan(`https://test.partial_ech.org/server-timing`); + await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL); + + await trrServer.stop(); +}); + +add_task(async function testFastfallbackWithoutEchConfig() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 50 + ); + + await trrServer.registerDoHAnswers("test.no_ech_h2.org", "HTTPS", { + answers: [ + { + name: "test.no_ech_h2.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.no_ech_h3.org", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("test.no_ech_h3.org", "A", { + answers: [ + { + name: "test.no_ech_h3.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await trrServer.registerDoHAnswers("test.no_ech_h2.org", "A", { + answers: [ + { + name: "test.no_ech_h2.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + let chan = makeChan(`https://test.no_ech_h2.org:${h2Port}/server-timing`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + + await trrServer.stop(); +}); + +add_task(async function testH3FallbackWithMultipleTransactions() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", false); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + + // Disable fast fallback. + Services.prefs.setIntPref( + "network.http.http3.parallel_fallback_conn_limit", + 0 + ); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + + await trrServer.registerDoHAnswers("test.multiple_trans.org", "HTTPS", { + answers: [ + { + name: "test.multiple_trans.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.multiple_trans.org", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("test.multiple_trans.org", "A", { + answers: [ + { + name: "test.multiple_trans.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + let promises = []; + for (let i = 0; i < 2; ++i) { + let chan = makeChan( + `https://test.multiple_trans.org:${h2Port}/server-timing` + ); + promises.push(channelOpenPromise(chan)); + } + + let res = await Promise.all(promises); + res.forEach(function (e) { + let [req] = e; + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + }); + + await trrServer.stop(); +}); + +add_task(async function testTwoFastFallbackTimers() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", false); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + Services.prefs.clearUserPref( + "network.http.http3.parallel_fallback_conn_limit" + ); + + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "foo.fallback.org;h3-29=:" + h3Port + ); + + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 10 + ); + Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 100); + + await trrServer.registerDoHAnswers("foo.fallback.org", "HTTPS", { + answers: [ + { + name: "foo.fallback.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "foo.fallback.org", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("foo.fallback.org", "A", { + answers: [ + { + name: "foo.fallback.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + // Test the case that http3 backup timer is triggered after + // fast fallback timer or HTTPS RR. + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 10 + ); + Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 100); + + async function createChannelAndStartTest() { + let chan = makeChan(`https://foo.fallback.org:${h2Port}/server-timing`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + } + + await createChannelAndStartTest(); + + Services.obs.notifyObservers(null, "net:prune-all-connections"); + Services.obs.notifyObservers(null, "network:reset-http3-excluded-list"); + Services.dns.clearCache(true); + + // Do the same test again, but with a different configuration. + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 100 + ); + Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 10); + + await createChannelAndStartTest(); + + await trrServer.stop(); +}); + +add_task(async function testH3FastFallbackWithMultipleTransactions() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", false); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + Services.prefs.clearUserPref( + "network.http.http3.parallel_fallback_conn_limit" + ); + + Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 500); + + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "test.multiple_fallback_trans.org;h3-29=:" + h3Port + ); + + await trrServer.registerDoHAnswers("test.multiple_fallback_trans.org", "A", { + answers: [ + { + name: "test.multiple_fallback_trans.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + let promises = []; + for (let i = 0; i < 3; ++i) { + let chan = makeChan( + `https://test.multiple_fallback_trans.org:${h2Port}/server-timing` + ); + if (i == 0) { + promises.push(channelOpenPromise(chan)); + } else { + promises.push(channelOpenPromise(chan, null, 500)); + } + } + + let res = await Promise.all(promises); + res.forEach(function (e) { + let [req] = e; + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + }); + + await trrServer.stop(); +}); + +add_task(async function testFastfallbackToTheSameRecord() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 1000 + ); + + await trrServer.registerDoHAnswers("test.ech.org", "HTTPS", { + answers: [ + { + name: "test.ech.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.ech1.org", + values: [ + { key: "alpn", value: ["h3-29", "h2"] }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("test.ech1.org", "A", { + answers: [ + { + name: "test.ech1.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + let chan = makeChan(`https://test.ech.org/server-timing`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_http3_fatal_stream_error.js b/netwerk/test/unit/test_http3_fatal_stream_error.js new file mode 100644 index 0000000000..4c0b41089a --- /dev/null +++ b/netwerk/test/unit/test_http3_fatal_stream_error.js @@ -0,0 +1,135 @@ +/* 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"; + +let httpsUri; + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.http3.enable"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.dns.disableIPv6"); + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); + Services.prefs.clearUserPref("network.http.http3.backup_timer_delay"); + dump("cleanup done\n"); +}); + +let Http3FailedListener = function () {}; + +Http3FailedListener.prototype = { + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.amount += cnt; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + if (Components.isSuccessCode(status)) { + // This is still HTTP2 connection + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h2"); + this.finish(false); + } else { + Assert.equal(status, Cr.NS_ERROR_NET_PARTIAL_TRANSFER); + this.finish(true); + } + }, +}; + +function makeChan() { + let chan = NetUtil.newChannel({ + uri: httpsUri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function altsvcSetupPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +add_task(async function test_fatal_error() { + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + + let h3Port = Services.env.get("MOZHTTP3_PORT_FAILED"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "foo.example.com;h3-29=:" + h3Port + ); + Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 0); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + httpsUri = "https://foo.example.com:" + h2Port + "/"; +}); + +add_task(async function test_fatal_stream_error() { + let result = false; + // We need to loop here because we need to wait for AltSvc storage to + // to be started. + do { + // We need to close HTTP2 connections, otherwise our connection pooling + // will dispatch the request over already existing HTTP2 connection. + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(); + let listener = new Http3FailedListener(); + result = await altsvcSetupPromise(chan, listener); + } while (result === false); +}); + +let CheckOnlyHttp2Listener = function () {}; + +CheckOnlyHttp2Listener.prototype = { + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h2"); + this.finish(false); + }, +}; + +add_task(async function test_no_http3_after_error() { + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); +}); + +// also after all connections are closed. +add_task(async function test_no_http3_after_error2() { + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); +}); diff --git a/netwerk/test/unit/test_http3_large_post.js b/netwerk/test/unit/test_http3_large_post.js new file mode 100644 index 0000000000..2ed8e51bb4 --- /dev/null +++ b/netwerk/test/unit/test_http3_large_post.js @@ -0,0 +1,165 @@ +/* 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/. */ + +registerCleanupFunction(async () => { + http3_clear_prefs(); +}); + +add_task(async function setup() { + await http3_setup_tests("h3-29"); +}); + +let Http3Listener = function (amount) { + this.amount = amount; +}; + +Http3Listener.prototype = { + expectedStatus: Cr.NS_OK, + amount: 0, + onProgressMaxNotificationCount: 0, + onProgressNotificationCount: 0, + + QueryInterface: ChromeUtils.generateQI(["nsIProgressEventSink"]), + + getInterface(iid) { + if (iid.equals(Ci.nsIProgressEventSink)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + onProgress(request, progress, progressMax) { + // we will get notifications for the request and the response. + if (progress === progressMax) { + this.onProgressMaxNotificationCount += 1; + } + // For a large upload there should be a multiple notifications. + this.onProgressNotificationCount += 1; + }, + + onStatus(request, status, statusArg) {}, + + onStartRequest: function testOnStartRequest(request) { + Assert.equal(request.status, this.expectedStatus); + if (Components.isSuccessCode(this.expectedStatus)) { + Assert.equal(request.responseStatus, 200); + } + Assert.equal( + this.amount, + request.getResponseHeader("x-data-received-length") + ); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + // We should get 2 correctOnProgress, i.e. one for request and one for the response. + Assert.equal(this.onProgressMaxNotificationCount, 2); + if (this.amount > 500000) { + // 10 is an arbitrary number, but we cannot calculate exact number + // because it depends on timing. + Assert.ok(this.onProgressNotificationCount > 10); + } + this.finish(); + }, +}; + +function chanPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function makeChan(uri, amount) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = generateContent(amount); + let uchan = chan.QueryInterface(Ci.nsIUploadChannel); + uchan.setUploadStream(stream, "text/plain", stream.available()); + chan.requestMethod = "POST"; + return chan; +} + +// Generate a post. +function generateContent(size) { + let content = ""; + for (let i = 0; i < size; i++) { + content += "0"; + } + return content; +} + +// Send a large post that can fit into a neqo stream buffer at once. +// But the amount of data is larger than the necko's default stream +// buffer side, therefore ReadSegments will be called multiple times. +add_task(async function test_large_post() { + let amount = 1 << 16; + let listener = new Http3Listener(amount); + let chan = makeChan("https://foo.example.com/post", amount); + chan.notificationCallbacks = listener; + await chanPromise(chan, listener); +}); + +// Send a large post that cannot fit into a neqo stream buffer at once. +// StreamWritable events will trigger sending more data when the buffer +// space is freed +add_task(async function test_large_post2() { + let amount = 1 << 23; + let listener = new Http3Listener(amount); + let chan = makeChan("https://foo.example.com/post", amount); + chan.notificationCallbacks = listener; + await chanPromise(chan, listener); +}); + +// Send a post in the same way viaduct does in bug 1749957. +add_task(async function test_bug1749957_bug1750056() { + let amount = 200; // Tests the bug as long as it's non-zero. + let uri = "https://foo.example.com/post"; + let listener = new Http3Listener(amount); + + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + + // https://searchfox.org/mozilla-central/rev/1920b17ac5988fcfec4e45e2a94478ebfbfc6f88/toolkit/components/viaduct/ViaductRequest.cpp#120-152 + { + chan.requestMethod = "POST"; + chan.setRequestHeader("content-length", "" + amount, /* aMerge = */ false); + + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = generateContent(amount); + let uchan = chan.QueryInterface(Ci.nsIUploadChannel2); + uchan.explicitSetUploadStream( + stream, + /* aContentType = */ "", + /* aContentLength = */ -1, + "POST", + /* aStreamHasHeaders = */ false + ); + } + + chan.notificationCallbacks = listener; + await chanPromise(chan, listener); +}); diff --git a/netwerk/test/unit/test_http3_large_post_telemetry.js b/netwerk/test/unit/test_http3_large_post_telemetry.js new file mode 100644 index 0000000000..33ad4b7d21 --- /dev/null +++ b/netwerk/test/unit/test_http3_large_post_telemetry.js @@ -0,0 +1,151 @@ +/* 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/. */ + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +let indexes_10_100 = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 27, 30, + 33, 37, 41, 46, 51, 57, 63, 70, 78, 87, 97, 108, 120, 133, 148, 165, 184, 205, + 228, 254, 282, 314, 349, 388, 431, 479, 533, 593, 659, 733, 815, 906, 1008, + 1121, 1247, 1387, 1542, 1715, 1907, 2121, 2359, 2623, 2917, 3244, 3607, 4011, + 4460, 4960, 5516, 6134, 6821, 7585, 8435, 9380, 10431, 11600, 12900, 14345, + 15952, 17739, 19727, 21937, 24395, 27129, 30169, 33549, 37308, 41488, 46137, + 51307, 57056, 63449, 70559, 78465, 87257, 97035, 107908, 120000, +]; + +let indexes_gt_100 = [ + 0, 30000, 30643, 31300, 31971, 32657, 33357, 34072, 34803, 35549, 36311, + 37090, 37885, 38697, 39527, 40375, 41241, 42125, 43028, 43951, 44894, 45857, + 46840, 47845, 48871, 49919, 50990, 52084, 53201, 54342, 55507, 56697, 57913, + 59155, 60424, 61720, 63044, 64396, 65777, 67188, 68629, 70101, 71604, 73140, + 74709, 76311, 77948, 79620, 81327, 83071, 84853, 86673, 88532, 90431, 92370, + 94351, 96374, 98441, 100552, 102708, 104911, 107161, 109459, 111806, 114204, + 116653, 119155, 121710, 124320, 126986, 129709, 132491, 135332, 138234, + 141199, 144227, 147320, 150479, 153706, 157002, 160369, 163808, 167321, + 170909, 174574, 178318, 182142, 186048, 190038, 194114, 198277, 202529, + 206872, 211309, 215841, 220470, 225198, 230028, 234961, 240000, +]; + +registerCleanupFunction(async () => { + http3_clear_prefs(); + Services.prefs.clearUserPref( + "toolkit.telemetry.testing.overrideProductsCheck" + ); +}); + +add_task(async function setup() { + // Enable the collection (during test) for all products so even products + // that don't collect the data will be able to run the test without failure. + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + await http3_setup_tests("h3-29"); +}); + +let Http3Listener = function () {}; + +Http3Listener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + this.finish(); + }, +}; + +function chanPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function makeChan(uri, amount) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = generateContent(amount); + let uchan = chan.QueryInterface(Ci.nsIUploadChannel); + uchan.setUploadStream(stream, "text/plain", stream.available()); + chan.requestMethod = "POST"; + return chan; +} + +// Generate a post. +function generateContent(size) { + let content = ""; + for (let i = 0; i < size; i++) { + content += "0"; + } + return content; +} + +async function test_large_post(amount, hist_name, key, indexes) { + let hist = TelemetryTestUtils.getAndClearKeyedHistogram(hist_name); + + let listener = new Http3Listener(); + listener.amount = amount; + let chan = makeChan("https://foo.example.com/post", amount); + let tchan = chan.QueryInterface(Ci.nsITimedChannel); + tchan.timingEnabled = true; + await chanPromise(chan, listener); + + let time = (tchan.responseStartTime - tchan.requestStartTime) / 1000; + let i = 0; + while (i < indexes.length && time > indexes[i + 1]) { + i += 1; + } + TelemetryTestUtils.assertKeyedHistogramValue(hist, key, indexes[i], 1); +} + +add_task(async function test_11M() { + await test_large_post( + 11 * (1 << 20), + "HTTP3_UPLOAD_TIME_10M_100M", + "uses_http3_10_50", + indexes_10_100 + ); +}); + +add_task(async function test_51M() { + await test_large_post( + 51 * (1 << 20), + "HTTP3_UPLOAD_TIME_10M_100M", + "uses_http3_50_100", + indexes_10_100 + ); +}); + +add_task(async function test_101M() { + await test_large_post( + 101 * (1 << 20), + "HTTP3_UPLOAD_TIME_GT_100M", + "uses_http3", + indexes_gt_100 + ); +}); diff --git a/netwerk/test/unit/test_http3_perf.js b/netwerk/test/unit/test_http3_perf.js new file mode 100644 index 0000000000..0a813cdf19 --- /dev/null +++ b/netwerk/test/unit/test_http3_perf.js @@ -0,0 +1,262 @@ +"use strict"; + +// This test is run as part of the perf tests which require the metadata. +/* exported perfMetadata */ +var perfMetadata = { + owner: "Network Team", + name: "http3 raw", + description: + "XPCShell tests that verifies the lib integration against a local server", + options: { + default: { + perfherder: true, + perfherder_metrics: [ + { + name: "speed", + unit: "bps", + }, + ], + xpcshell_cycles: 13, + verbose: true, + try_platform: ["linux", "mac"], + }, + }, + tags: ["network", "http3", "quic"], +}; + +var performance = performance || {}; +performance.now = (function () { + return ( + performance.now || + performance.mozNow || + performance.msNow || + performance.oNow || + performance.webkitNow || + Date.now + ); +})(); + +let h3Route; +let httpsOrigin; +let h3AltSvc; + +let prefs; + +let tests = [ + // This test must be the first because it setsup alt-svc connection, that + // other tests use. + test_https_alt_svc, + test_download, + testsDone, +]; + +let current_test = 0; + +function run_next_test() { + if (current_test < tests.length) { + dump("starting test number " + current_test + "\n"); + tests[current_test](); + current_test++; + } +} + +// eslint-disable-next-line no-unused-vars +function run_test() { + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + let h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + h3AltSvc = ":" + h3Port; + + h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + prefs = Services.prefs; + + prefs.setBoolPref("network.http.http3.enable", true); + prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + // We always resolve elements of localDomains as it's hardcoded without the + // following pref: + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + + // The certificate for the http3server server is for foo.example.com and + // is signed by http2-ca.pem so add that cert to the trust list as a + // signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + httpsOrigin = "https://foo.example.com:" + h2Port + "/"; + + run_next_test(); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let Http3CheckListener = function () {}; + +Http3CheckListener.prototype = { + onDataAvailableFired: false, + expectedRoute: "", + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + dump("status is " + status + "\n"); + // Assert.equal(status, Cr.NS_OK); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, this.expectedRoute); + + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + Assert.equal(this.onDataAvailableFired, true); + }, +}; + +let WaitForHttp3Listener = function () {}; + +WaitForHttp3Listener.prototype = new Http3CheckListener(); + +WaitForHttp3Listener.prototype.uri = ""; +WaitForHttp3Listener.prototype.h3AltSvc = ""; + +WaitForHttp3Listener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + Assert.equal(status, Cr.NS_OK); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + if (routed == this.expectedRoute) { + Assert.equal(routed, this.expectedRoute); // always true, but a useful log + + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + run_next_test(); + } else { + dump("poll later for alt svc mapping\n"); + do_test_pending(); + do_timeout(500, () => { + doTest(this.uri, this.expectedRoute, this.h3AltSvc); + }); + } + + do_test_finished(); +}; + +function doTest(uri, expectedRoute, altSvc) { + let chan = makeChan(uri); + let listener = new WaitForHttp3Listener(); + listener.uri = uri; + listener.expectedRoute = expectedRoute; + listener.h3AltSvc = altSvc; + chan.setRequestHeader("x-altsvc", altSvc, false); + chan.asyncOpen(listener); +} + +// Test Alt-Svc for http3. +// H2 server returns alt-svc=h3-29=:h3port +function test_https_alt_svc() { + dump("test_https_alt_svc()\n"); + + do_test_pending(); + doTest(httpsOrigin + "http3-test", h3Route, h3AltSvc); +} + +let PerfHttp3Listener = function () {}; + +PerfHttp3Listener.prototype = new Http3CheckListener(); +PerfHttp3Listener.prototype.amount = 0; +PerfHttp3Listener.prototype.bytesRead = 0; +PerfHttp3Listener.prototype.startTime = 0; + +PerfHttp3Listener.prototype.onStartRequest = function testOnStartRequest( + request +) { + this.startTime = performance.now(); + Http3CheckListener.prototype.onStartRequest.call(this, request); +}; + +PerfHttp3Listener.prototype.onDataAvailable = function testOnStopRequest( + request, + stream, + off, + cnt +) { + this.bytesRead += cnt; + Http3CheckListener.prototype.onDataAvailable.call( + this, + request, + stream, + off, + cnt + ); +}; + +PerfHttp3Listener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + let stopTime = performance.now(); + Http3CheckListener.prototype.onStopRequest.call(this, request, status); + if (this.bytesRead != this.amount) { + dump("Wrong number of bytes..."); + } else { + let speed = (this.bytesRead * 1000) / (stopTime - this.startTime); + info("perfMetrics", { speed }); + } + run_next_test(); + do_test_finished(); +}; + +function test_download() { + dump("test_download()\n"); + + let listener = new PerfHttp3Listener(); + listener.expectedRoute = h3Route; + listener.amount = 1024 * 1024; + let chan = makeChan(httpsOrigin + listener.amount.toString()); + chan.asyncOpen(listener); + do_test_pending(); +} + +function testsDone() { + prefs.clearUserPref("network.http.http3.enable"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + dump("testDone\n"); + do_test_pending(); + do_test_finished(); +} diff --git a/netwerk/test/unit/test_http3_prio_disabled.js b/netwerk/test/unit/test_http3_prio_disabled.js new file mode 100644 index 0000000000..b73ca98709 --- /dev/null +++ b/netwerk/test/unit/test_http3_prio_disabled.js @@ -0,0 +1,106 @@ +/* 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 test file can be run directly as a part of parent/main process +// or indirectly from the wrapper test file as a part of child/content process + +// need to get access to helper functions/structures +// load ensures +// * testing environment is available (ie Assert.ok()) +/*global inChildProcess, test_flag_priority */ +load("../unit/test_http3_prio_helpers.js"); + +// direct call to this test file should cleanup after itself +// otherwise the wrapper will handle +if (!inChildProcess()) { + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.http3.priority"); + http3_clear_prefs(); + }); +} + +// setup once, before tests +add_task(async function setup() { + // wrapper handles when testing as content process for pref change + if (!inChildProcess()) { + await http3_setup_tests("h3-29"); + } +}); + +// tests various flags when priority has been disabled on variable incremental +// this function should only be called the preferences priority disabled +async function test_http3_prio_disabled(incremental) { + await test_flag_priority("disabled (none)", null, null, null, null); // default-test + await test_flag_priority( + "disabled (urgent_start)", + Ci.nsIClassOfService.UrgentStart, + null, + incremental, + null + ); + await test_flag_priority( + "disabled (leader)", + Ci.nsIClassOfService.Leader, + null, + incremental, + null + ); + await test_flag_priority( + "disabled (unblocked)", + Ci.nsIClassOfService.Unblocked, + null, + incremental, + null + ); + await test_flag_priority( + "disabled (follower)", + Ci.nsIClassOfService.Follower, + null, + incremental, + null + ); + await test_flag_priority( + "disabled (speculative)", + Ci.nsIClassOfService.Speculative, + null, + incremental, + null + ); + await test_flag_priority( + "disabled (background)", + Ci.nsIClassOfService.Background, + null, + incremental, + null + ); + await test_flag_priority( + "disabled (background)", + Ci.nsIClassOfService.Tail, + null, + incremental, + null + ); +} + +// run tests after setup + +// test that various urgency flags and incremental=true don't propogate to header +// when priority setting is disabled +add_task(async function test_http3_prio_disabled_inc_true() { + // wrapper handles when testing as content process for pref change + if (!inChildProcess()) { + Services.prefs.setBoolPref("network.http.http3.priority", false); + } + await test_http3_prio_disabled(true); +}); + +// test that various urgency flags and incremental=false don't propogate to header +// when priority setting is disabled +add_task(async function test_http3_prio_disabled_inc_false() { + // wrapper handles when testing as content process for pref change + if (!inChildProcess()) { + Services.prefs.setBoolPref("network.http.http3.priority", false); + } + await test_http3_prio_disabled(false); +}); diff --git a/netwerk/test/unit/test_http3_prio_enabled.js b/netwerk/test/unit/test_http3_prio_enabled.js new file mode 100644 index 0000000000..6dd30c590a --- /dev/null +++ b/netwerk/test/unit/test_http3_prio_enabled.js @@ -0,0 +1,108 @@ +/* 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 test file can be run directly as a part of parent/main process +// or indirectly from the wrapper test file as a part of child/content process + +// need to get access to helper functions/structures +// load ensures +// * testing environment is available (ie Assert.ok()) +/*global inChildProcess, test_flag_priority */ +load("../unit/test_http3_prio_helpers.js"); + +// direct call to this test file should cleanup after itself +// otherwise the wrapper will handle +if (!inChildProcess()) { + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.http3.priority"); + http3_clear_prefs(); + }); +} + +// setup once, before tests +add_task(async function setup() { + // wrapper handles when testing as content process for pref change + if (!inChildProcess()) { + await http3_setup_tests("h3-29"); + } +}); + +// tests various flags when priority has been enabled on variable incremental +// this function should only be called the preferences priority disabled +async function test_http3_prio_enabled(incremental) { + await test_flag_priority("enabled (none)", null, "u=4", null, false); // default-test + await test_flag_priority( + "enabled (urgent_start)", + Ci.nsIClassOfService.UrgentStart, + "u=1", + incremental, + incremental + ); + await test_flag_priority( + "enabled (leader)", + Ci.nsIClassOfService.Leader, + "u=2", + incremental, + incremental + ); + + // if priority-urgency and incremental are both default values + // then we shouldn't expect to see the priority header at all + // hence when: + // incremental=true -> we expect incremental + // incremental=false -> we expect null + await test_flag_priority( + "enabled (unblocked)", + Ci.nsIClassOfService.Unblocked, + null, + incremental, + incremental ? incremental : null + ); + + await test_flag_priority( + "enabled (follower)", + Ci.nsIClassOfService.Follower, + "u=4", + incremental, + incremental + ); + await test_flag_priority( + "enabled (speculative)", + Ci.nsIClassOfService.Speculative, + "u=6", + incremental, + incremental + ); + await test_flag_priority( + "enabled (background)", + Ci.nsIClassOfService.Background, + "u=6", + incremental, + incremental + ); + await test_flag_priority( + "enabled (background)", + Ci.nsIClassOfService.Tail, + "u=6", + incremental, + incremental + ); +} + +// with priority enabled: test urgency flags with both incremental enabled and disabled +add_task(async function test_http3_prio_enabled_incremental_true() { + // wrapper handles when testing as content process for pref change + if (!inChildProcess()) { + Services.prefs.setBoolPref("network.http.http3.priority", true); + } + await test_http3_prio_enabled(true); +}); + +add_task(async function test_http3_prio_enabled_incremental_false() { + // wrapper handles when testing as content process for pref change + if (!inChildProcess()) { + Services.prefs.setBoolPref("network.http.http3.priority", true); + } + await test_http3_prio_enabled(false); +}); diff --git a/netwerk/test/unit/test_http3_prio_helpers.js b/netwerk/test/unit/test_http3_prio_helpers.js new file mode 100644 index 0000000000..c1f6d06bcb --- /dev/null +++ b/netwerk/test/unit/test_http3_prio_helpers.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/. */ + +// uses head_http3.js, which uses http2-ca.pem +"use strict"; + +/* exported inChildProcess, test_flag_priority */ +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +let Http3Listener = function ( + closure, + expected_priority, + expected_incremental, + context +) { + this._closure = closure; + this._expected_priority = expected_priority; + this._expected_incremental = expected_incremental; + this._context = context; +}; + +// string -> [string, bool] +// "u=3,i" -> ["u=3", true] +function parse_priority_response_header(priority) { + const priority_array = priority.split(","); + + // parse for urgency string + const urgency = priority_array.find(element => element.includes("u=")); + + // parse for incremental bool + const incremental = !!priority_array.find(element => element == "i"); + + return [urgency ? urgency : null, incremental]; +} + +Http3Listener.prototype = { + resumed: false, + + onStartRequest: function testOnStartRequest(request) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(request.responseStatus, 200); + + let secinfo = request.securityInfo; + Assert.equal(secinfo.resumed, this.resumed); + Assert.ok(secinfo.serverCert != null); + + // check priority urgency and incremental from response header + let priority_urgency = null; + let incremental = null; + try { + const prh = request.getResponseHeader("priority-mirror"); + [priority_urgency, incremental] = parse_priority_response_header(prh); + } catch (e) { + console.log("Failed to get priority-mirror from response header"); + } + Assert.equal(priority_urgency, this._expected_priority, this._context); + Assert.equal(incremental, this._expected_incremental, this._context); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + + try { + this._closure(); + } catch (ex) { + do_throw("Error in closure function: " + ex); + } + }, +}; + +function make_channel(url) { + var request = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + request.QueryInterface(Ci.nsIHttpChannel); + return request; +} + +async function test_flag_priority( + context, + flag, + expected_priority, + incremental, + expected_incremental +) { + var chan = make_channel("https://foo.example.com/priority_mirror"); + var cos = chan.QueryInterface(Ci.nsIClassOfService); + + // configure the channel with flags + if (flag != null) { + cos.addClassFlags(flag); + } + + // configure the channel with incremental + if (incremental != null) { + cos.incremental = incremental; + } + + await new Promise(resolve => + chan.asyncOpen( + new Http3Listener( + resolve, + expected_priority, + expected_incremental, + context + ) + ) + ); +} diff --git a/netwerk/test/unit/test_http3_server.js b/netwerk/test/unit/test_http3_server.js new file mode 100644 index 0000000000..5c1b081c52 --- /dev/null +++ b/netwerk/test/unit/test_http3_server.js @@ -0,0 +1,168 @@ +/* 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"; + +let h2Port; +let h3Port; +let trrServer; + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +add_setup(async function setup() { + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + h3Port = Services.env.get("MOZHTTP3_PORT_PROXY"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + trr_test_setup(); + + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + + registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + Services.prefs.clearUserPref("network.dns.disablePrefetch"); + await trrServer.stop(); + }); + + if (mozinfo.socketprocess_networking) { + Services.dns; + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + resolve([req, buffer]); + } + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +// Use NodeHTTPServer to create an HTTP server and test if the Http/3 server +// can act as a reverse proxy. +add_task(async function testHttp3ServerAsReverseProxy() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.h3_example.com", "HTTPS", { + answers: [ + { + name: "test.h3_example.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.h3_example.com", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("test.h3_example.com", "A", { + answers: [ + { + name: "test.h3_example.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await new TRRDNSListener("test.h3_example.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let server = new NodeHTTPServer(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + + await server.registerPathHandler("/test", (req, resp) => { + if (req.method === "GET") { + resp.writeHead(200); + resp.end("got GET request"); + } else if (req.method === "POST") { + let received = ""; + req.on("data", function receivePostData(chunk) { + received += chunk.toString(); + }); + req.on("end", function finishPost() { + resp.writeHead(200); + resp.end(received); + }); + } + }); + + // Tell the Http/3 server which port to forward requests. + let chan = makeChan(`https://test.h3_example.com/port?${server.port()}`); + await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + + // Test GET method. + chan = makeChan(`https://test.h3_example.com/test`); + let [req, buf] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + Assert.equal(req.protocolVersion, "h3-29"); + Assert.equal(buf, "got GET request"); + + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = "b".repeat(500); + + // Test POST method. + chan = makeChan(`https://test.h3_example.com/test`); + chan + .QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(stream, "text/plain", stream.available()); + chan.requestMethod = "POST"; + + [req, buf] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + Assert.equal(req.protocolVersion, "h3-29"); + Assert.equal(buf, stream.data); + + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_http3_server_not_existing.js b/netwerk/test/unit/test_http3_server_not_existing.js new file mode 100644 index 0000000000..b2b5518974 --- /dev/null +++ b/netwerk/test/unit/test_http3_server_not_existing.js @@ -0,0 +1,109 @@ +/* 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"; + +let httpsUri; + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.http3.enable"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.dns.disableIPv6"); + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); + Services.prefs.clearUserPref("network.http.http3.backup_timer_delay"); + dump("cleanup done\n"); +}); + +function makeChan() { + let chan = NetUtil.newChannel({ + uri: httpsUri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function altsvcSetupPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +add_task(async function test_fatal_error() { + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + // Set AltSvc to point to not existing HTTP3 server on port 443 + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "foo.example.com;h3-29=:443" + ); + Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 0); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + httpsUri = "https://foo.example.com:" + h2Port + "/"; +}); + +add_task(async function test_fatal_stream_error() { + let result = 1; + // We need to loop here because we need to wait for AltSvc storage to + // to be started. + // We also do not have a way to verify that HTTP3 has been tried, because + // the fallback is automatic, so try a couple of times. + do { + // We need to close HTTP2 connections, otherwise our connection pooling + // will dispatch the request over already existing HTTP2 connection. + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); + result++; + } while (result < 5); +}); + +let CheckOnlyHttp2Listener = function () {}; + +CheckOnlyHttp2Listener.prototype = { + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h2"); + this.finish(); + }, +}; + +add_task(async function test_no_http3_after_error() { + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); +}); + +// also after all connections are closed. +add_task(async function test_no_http3_after_error2() { + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); +}); diff --git a/netwerk/test/unit/test_http3_trans_close.js b/netwerk/test/unit/test_http3_trans_close.js new file mode 100644 index 0000000000..0bbb183aab --- /dev/null +++ b/netwerk/test/unit/test_http3_trans_close.js @@ -0,0 +1,84 @@ +/* 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/. */ + +registerCleanupFunction(async () => { + http3_clear_prefs(); +}); + +add_task(async function setup() { + await http3_setup_tests("h3-29"); +}); + +let Http3Listener = function () {}; + +Http3Listener.prototype = { + expectedAmount: 0, + expectedStatus: Cr.NS_OK, + amount: 0, + + onStartRequest: function testOnStartRequest(request) { + Assert.equal(request.status, this.expectedStatus); + if (Components.isSuccessCode(this.expectedStatus)) { + Assert.equal(request.responseStatus, 200); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.amount += cnt; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3-29"); + Assert.equal(this.amount, this.expectedAmount); + + this.finish(); + }, +}; + +function chanPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +add_task(async function test_response_without_body() { + let chan = makeChan("https://foo.example.com/no_body"); + let listener = new Http3Listener(); + listener.expectedAmount = 0; + await chanPromise(chan, listener); +}); + +add_task(async function test_response_without_content_length() { + let chan = makeChan("https://foo.example.com/no_content_length"); + let listener = new Http3Listener(); + listener.expectedAmount = 4000; + await chanPromise(chan, listener); +}); + +add_task(async function test_content_length_smaller_than_data_len() { + let chan = makeChan("https://foo.example.com/content_length_smaller"); + let listener = new Http3Listener(); + // content-lentgth is 4000, but data length is 8000. + // We should return an error here - bug 1670086. + listener.expectedAmount = 4000; + await chanPromise(chan, listener); +}); diff --git a/netwerk/test/unit/test_http3_version1.js b/netwerk/test/unit/test_http3_version1.js new file mode 100644 index 0000000000..65f4eef906 --- /dev/null +++ b/netwerk/test/unit/test_http3_version1.js @@ -0,0 +1,93 @@ +/* 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"; + +registerCleanupFunction(async () => { + http3_clear_prefs(); +}); + +let httpsUri; + +add_task(async function pre_setup() { + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + httpsUri = "https://foo.example.com:" + h2Port + "/"; + Services.prefs.setBoolPref("network.http.http3.support_version1", true); +}); + +add_task(async function setup() { + await http3_setup_tests("h3"); +}); + +function chanPromise(chan, listener) { + return new Promise(resolve => { + function finish() { + resolve(); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function makeH2Chan() { + let chan = NetUtil.newChannel({ + uri: httpsUri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let Http3Listener = function () {}; + +Http3Listener.prototype = { + version1enabled: "", + + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + if (this.version1enabled) { + Assert.equal(httpVersion, "h3"); + } else { + Assert.equal(httpVersion, "h2"); + } + + this.finish(); + }, +}; + +add_task(async function test_version1_enabled_1() { + Services.prefs.setBoolPref("network.http.http3.support_version1", true); + let listener = new Http3Listener(); + listener.version1enabled = true; + let chan = makeH2Chan("https://foo.example.com/"); + await chanPromise(chan, listener); +}); + +add_task(async function test_version1_disabled() { + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + Services.prefs.setBoolPref("network.http.http3.support_version1", false); + let listener = new Http3Listener(); + listener.version1enabled = false; + let chan = makeH2Chan("https://foo.example.com/"); + await chanPromise(chan, listener); +}); + +add_task(async function test_version1_enabled_2() { + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + Services.prefs.setBoolPref("network.http.http3.support_version1", true); + let listener = new Http3Listener(); + listener.version1enabled = true; + let chan = makeH2Chan("https://foo.example.com/"); + await chanPromise(chan, listener); +}); diff --git a/netwerk/test/unit/test_httpResponseTimeout.js b/netwerk/test/unit/test_httpResponseTimeout.js new file mode 100644 index 0000000000..24713a7aea --- /dev/null +++ b/netwerk/test/unit/test_httpResponseTimeout.js @@ -0,0 +1,160 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var baseURL; +const kResponseTimeoutPref = "network.http.response.timeout"; +const kResponseTimeout = 1; +const kShortLivedKeepalivePref = + "network.http.tcp_keepalive.short_lived_connections"; +const kLongLivedKeepalivePref = + "network.http.tcp_keepalive.long_lived_connections"; + +const prefService = Services.prefs; + +var server = new HttpServer(); + +function TimeoutListener(expectResponse) { + this.expectResponse = expectResponse; +} + +TimeoutListener.prototype = { + onStartRequest(request) {}, + + onDataAvailable(request, stream) {}, + + onStopRequest(request, status) { + if (this.expectResponse) { + Assert.equal(status, Cr.NS_OK); + } else { + Assert.equal(status, Cr.NS_ERROR_NET_TIMEOUT); + } + + run_next_test(); + }, +}; + +function serverStopListener() { + do_test_finished(); +} + +function testTimeout(timeoutEnabled, expectResponse) { + // Set timeout pref. + if (timeoutEnabled) { + prefService.setIntPref(kResponseTimeoutPref, kResponseTimeout); + } else { + prefService.setIntPref(kResponseTimeoutPref, 0); + } + + var chan = NetUtil.newChannel({ + uri: baseURL, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + var listener = new TimeoutListener(expectResponse); + chan.asyncOpen(listener); +} + +function testTimeoutEnabled() { + // Set a timeout value; expect a timeout and no response. + testTimeout(true, false); +} + +function testTimeoutDisabled() { + // Set a timeout value of 0; expect a response. + testTimeout(false, true); +} + +function testTimeoutDisabledByShortLivedKeepalives() { + // Enable TCP Keepalives for short lived HTTP connections. + prefService.setBoolPref(kShortLivedKeepalivePref, true); + prefService.setBoolPref(kLongLivedKeepalivePref, false); + + // Try to set a timeout value, but expect a response without timeout. + testTimeout(true, true); +} + +function testTimeoutDisabledByLongLivedKeepalives() { + // Enable TCP Keepalives for long lived HTTP connections. + prefService.setBoolPref(kShortLivedKeepalivePref, false); + prefService.setBoolPref(kLongLivedKeepalivePref, true); + + // Try to set a timeout value, but expect a response without timeout. + testTimeout(true, true); +} + +function testTimeoutDisabledByBothKeepalives() { + // Enable TCP Keepalives for short and long lived HTTP connections. + prefService.setBoolPref(kShortLivedKeepalivePref, true); + prefService.setBoolPref(kLongLivedKeepalivePref, true); + + // Try to set a timeout value, but expect a response without timeout. + testTimeout(true, true); +} + +function setup_tests() { + // Start tests with timeout enabled, i.e. disable TCP keepalives for HTTP. + // Reset pref in cleanup. + if (prefService.getBoolPref(kShortLivedKeepalivePref)) { + prefService.setBoolPref(kShortLivedKeepalivePref, false); + registerCleanupFunction(function () { + prefService.setBoolPref(kShortLivedKeepalivePref, true); + }); + } + if (prefService.getBoolPref(kLongLivedKeepalivePref)) { + prefService.setBoolPref(kLongLivedKeepalivePref, false); + registerCleanupFunction(function () { + prefService.setBoolPref(kLongLivedKeepalivePref, true); + }); + } + + var tests = [ + // Enable with a timeout value >0; + testTimeoutEnabled, + // Disable with a timeout value of 0; + testTimeoutDisabled, + // Disable by enabling TCP keepalive for short-lived HTTP connections. + testTimeoutDisabledByShortLivedKeepalives, + // Disable by enabling TCP keepalive for long-lived HTTP connections. + testTimeoutDisabledByLongLivedKeepalives, + // Disable by enabling TCP keepalive for both HTTP connection types. + testTimeoutDisabledByBothKeepalives, + ]; + + for (var i = 0; i < tests.length; i++) { + add_test(tests[i]); + } +} + +function setup_http_server() { + // Start server; will be stopped at test cleanup time. + server.start(-1); + baseURL = "http://localhost:" + server.identity.primaryPort + "/"; + info("Using baseURL: " + baseURL); + server.registerPathHandler("/", function (metadata, response) { + // Wait until the timeout should have passed, then respond. + response.processAsync(); + + do_timeout((kResponseTimeout + 1) * 1000 /* ms */, function () { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.write("Hello world"); + response.finish(); + }); + }); + registerCleanupFunction(function () { + server.stop(serverStopListener); + }); +} + +function run_test() { + setup_http_server(); + + setup_tests(); + + run_next_test(); +} diff --git a/netwerk/test/unit/test_http_408_retry.js b/netwerk/test/unit/test_http_408_retry.js new file mode 100644 index 0000000000..16392ab4bf --- /dev/null +++ b/netwerk/test/unit/test_http_408_retry.js @@ -0,0 +1,92 @@ +/* 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"; + +async function loadURL(uri, flags) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + + return new Promise(resolve => { + chan.asyncOpen( + new ChannelListener((req, buff) => resolve({ req, buff }), null, flags) + ); + }); +} + +add_task(async function test() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + async function check408retry(server) { + info(`Testing ${server.constructor.name}`); + await server.execute(`global.server_name = "${server.constructor.name}";`); + if ( + server.constructor.name == "NodeHTTPServer" || + server.constructor.name == "NodeHTTPSServer" + ) { + await server.registerPathHandler("/test", (req, resp) => { + let oldSock = global.socket; + global.socket = resp.socket; + if (global.socket == oldSock) { + // This function is handled within the httpserver where setTimeout is + // available. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout, no-undef + setTimeout( + arg => { + arg.writeHead(408); + arg.end("stuff"); + }, + 1100, + resp + ); + return; + } + resp.writeHead(200); + resp.end(global.server_name); + }); + } else { + await server.registerPathHandler("/test", (req, resp) => { + global.socket = resp.socket; + if (!global.sent408) { + global.sent408 = true; + resp.writeHead(408); + resp.end("stuff"); + return; + } + resp.writeHead(200); + resp.end(global.server_name); + }); + } + + async function load() { + let { req, buff } = await loadURL( + `${server.origin()}/test`, + CL_ALLOW_UNKNOWN_CL + ); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + equal(buff, server.constructor.name); + equal( + req.QueryInterface(Ci.nsIHttpChannel).protocolVersion, + server.constructor.name == "NodeHTTP2Server" ? "h2" : "http/1.1" + ); + } + + info("first load"); + await load(); + info("second load"); + await load(); + } + + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + check408retry + ); +}); diff --git a/netwerk/test/unit/test_http_headers.js b/netwerk/test/unit/test_http_headers.js new file mode 100644 index 0000000000..b43cebea1f --- /dev/null +++ b/netwerk/test/unit/test_http_headers.js @@ -0,0 +1,75 @@ +"use strict"; + +function check_request_header(chan, name, value) { + var chanValue; + try { + chanValue = chan.getRequestHeader(name); + } catch (e) { + do_throw("Expected to find header '" + name + "' but didn't find it"); + } + Assert.equal(chanValue, value); +} + +function run_test() { + var chan = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + + check_request_header(chan, "host", "www.mozilla.org"); + check_request_header(chan, "Host", "www.mozilla.org"); + + chan.setRequestHeader("foopy", "bar", false); + check_request_header(chan, "foopy", "bar"); + + chan.setRequestHeader("foopy", "baz", true); + check_request_header(chan, "foopy", "bar, baz"); + + for (let i = 0; i < 100; ++i) { + chan.setRequestHeader("foopy" + i, i, false); + } + + for (let i = 0; i < 100; ++i) { + check_request_header(chan, "foopy" + i, i); + } + + var x = false; + try { + chan.setRequestHeader("foo:py", "baz", false); + } catch (e) { + x = true; + } + if (!x) { + do_throw("header with colon not rejected"); + } + + x = false; + try { + chan.setRequestHeader("foopy", "b\naz", false); + } catch (e) { + x = true; + } + if (!x) { + do_throw("header value with newline not rejected"); + } + + x = false; + try { + chan.setRequestHeader("foopy\u0080", "baz", false); + } catch (e) { + x = true; + } + if (!x) { + do_throw("header name with non-ASCII not rejected"); + } + + x = false; + try { + chan.setRequestHeader("foopy", "b\u0000az", false); + } catch (e) { + x = true; + } + if (!x) { + do_throw("header value with null-byte not rejected"); + } +} diff --git a/netwerk/test/unit/test_http_server_timing.js b/netwerk/test/unit/test_http_server_timing.js new file mode 100644 index 0000000000..2e71c65c1e --- /dev/null +++ b/netwerk/test/unit/test_http_server_timing.js @@ -0,0 +1,138 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env node */ + +"use strict"; + +class ServerCode { + static async startServer(port) { + global.http = require("http"); + global.server = global.http.createServer((req, res) => { + res.setHeader("Content-Type", "text/plain"); + res.setHeader("Content-Length", "12"); + res.setHeader("Transfer-Encoding", "chunked"); + res.setHeader("Trailer", "Server-Timing"); + res.setHeader( + "Server-Timing", + "metric; dur=123.4; desc=description, metric2; dur=456.78; desc=description1" + ); + res.write("data reached"); + res.addTrailers({ + "Server-Timing": + "metric3; dur=789.11; desc=description2, metric4; dur=1112.13; desc=description3", + }); + res.end(); + }); + + let serverPort = await new Promise(resolve => { + global.server.listen(0, "0.0.0.0", 2000, () => { + resolve(global.server.address().port); + }); + }); + + if (process.env.MOZ_ANDROID_DATA_DIR) { + // When creating a server on Android we must make sure that the port + // is forwarded from the host machine to the emulator. + let adb_path = "adb"; + if (process.env.MOZ_FETCHES_DIR) { + adb_path = `${process.env.MOZ_FETCHES_DIR}/android-sdk-linux/platform-tools/adb`; + } + + await new Promise(resolve => { + const { exec } = require("child_process"); + exec( + `${adb_path} reverse tcp:${serverPort} tcp:${serverPort}`, + (error, stdout, stderr) => { + if (error) { + console.log(`error: ${error.message}`); + return; + } + if (stderr) { + console.log(`stderr: ${stderr}`); + } + // console.log(`stdout: ${stdout}`); + resolve(); + } + ); + }); + } + + return serverPort; + } +} + +const responseServerTiming = [ + { metric: "metric", duration: "123.4", description: "description" }, + { metric: "metric2", duration: "456.78", description: "description1" }, +]; +const trailerServerTiming = [ + { metric: "metric3", duration: "789.11", description: "description2" }, + { metric: "metric4", duration: "1112.13", description: "description3" }, +]; + +let port; + +add_task(async function setup() { + let processId = await NodeServer.fork(); + registerCleanupFunction(async () => { + await NodeServer.kill(processId); + }); + await NodeServer.execute(processId, ServerCode); + port = await NodeServer.execute(processId, `ServerCode.startServer(0)`); + ok(port); +}); + +// Test that secure origins can use server-timing, even with plain http +add_task(async function test_localhost_origin() { + let chan = NetUtil.newChannel({ + uri: `http://localhost:${port}/`, + loadUsingSystemPrincipal: true, + }); + await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener((request, buffer) => { + let channel = request.QueryInterface(Ci.nsITimedChannel); + let headers = channel.serverTiming.QueryInterface(Ci.nsIArray); + ok(headers.length); + + let expectedResult = responseServerTiming.concat(trailerServerTiming); + Assert.equal(headers.length, expectedResult.length); + + for (let i = 0; i < expectedResult.length; i++) { + let header = headers.queryElementAt(i, Ci.nsIServerTiming); + Assert.equal(header.name, expectedResult[i].metric); + Assert.equal(header.description, expectedResult[i].description); + Assert.equal(header.duration, parseFloat(expectedResult[i].duration)); + } + resolve(); + }, null) + ); + }); +}); + +// Test that insecure origins can't use server timing. +add_task(async function test_http_non_localhost() { + Services.prefs.setBoolPref("network.dns.native-is-localhost", true); + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.dns.native-is-localhost"); + }); + + let chan = NetUtil.newChannel({ + uri: `http://example.org:${port}/`, + loadUsingSystemPrincipal: true, + }); + await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener((request, buffer) => { + let channel = request.QueryInterface(Ci.nsITimedChannel); + let headers = channel.serverTiming.QueryInterface(Ci.nsIArray); + Assert.equal(headers.length, 0); + resolve(); + }, null) + ); + }); +}); diff --git a/netwerk/test/unit/test_http_sfv.js b/netwerk/test/unit/test_http_sfv.js new file mode 100644 index 0000000000..4b7ec9893c --- /dev/null +++ b/netwerk/test/unit/test_http_sfv.js @@ -0,0 +1,597 @@ +"use strict"; + +const gService = Cc["@mozilla.org/http-sfv-service;1"].getService( + Ci.nsISFVService +); + +add_task(async function test_sfv_bare_item() { + // tests bare item + let item_int = gService.newInteger(19); + Assert.equal(item_int.value, 19, "check bare item value"); + + let item_bool = gService.newBool(true); + Assert.equal(item_bool.value, true, "check bare item value"); + item_bool.value = false; + Assert.equal(item_bool.value, false, "check bare item value"); + + let item_float = gService.newDecimal(145.45); + Assert.equal(item_float.value, 145.45); + + let item_str = gService.newString("some_string"); + Assert.equal(item_str.value, "some_string", "check bare item value"); + + let item_byte_seq = gService.newByteSequence("aGVsbG8="); + Assert.equal(item_byte_seq.value, "aGVsbG8=", "check bare item value"); + + let item_token = gService.newToken("*a"); + Assert.equal(item_token.value, "*a", "check bare item value"); +}); + +add_task(async function test_sfv_params() { + // test params + let params = gService.newParameters(); + let bool_param = gService.newBool(false); + let int_param = gService.newInteger(15); + let decimal_param = gService.newDecimal(15.45); + + params.set("bool_param", bool_param); + params.set("int_param", int_param); + params.set("decimal_param", decimal_param); + + Assert.throws( + () => { + params.get("some_param"); + }, + /NS_ERROR_UNEXPECTED/, + "must throw exception as key does not exist in parameters" + ); + Assert.equal( + params.get("bool_param").QueryInterface(Ci.nsISFVBool).value, + false, + "get parameter by key and check its value" + ); + Assert.equal( + params.get("int_param").QueryInterface(Ci.nsISFVInteger).value, + 15, + "get parameter by key and check its value" + ); + Assert.equal( + params.get("decimal_param").QueryInterface(Ci.nsISFVDecimal).value, + 15.45, + "get parameter by key and check its value" + ); + Assert.deepEqual( + params.keys(), + ["bool_param", "int_param", "decimal_param"], + "check that parameters contain all the expected keys" + ); + + params.delete("int_param"); + Assert.deepEqual( + params.keys(), + ["bool_param", "decimal_param"], + "check that parameter has been deleted" + ); + + Assert.throws( + () => { + params.delete("some_param"); + }, + /NS_ERROR_UNEXPECTED/, + "must throw exception upon attempt to delete by non-existing key" + ); +}); + +add_task(async function test_sfv_inner_list() { + // create primitives for inner list + let item1_params = gService.newParameters(); + item1_params.set("param_1", gService.newToken("*smth")); + let item1 = gService.newItem(gService.newDecimal(172.145865), item1_params); + + let item2_params = gService.newParameters(); + item2_params.set("param_1", gService.newBool(true)); + item2_params.set("param_2", gService.newInteger(145454)); + let item2 = gService.newItem( + gService.newByteSequence("weather"), + item2_params + ); + + // create inner list + let inner_list_params = gService.newParameters(); + inner_list_params.set("inner_param", gService.newByteSequence("tests")); + let inner_list = gService.newInnerList([item1, item2], inner_list_params); + + // check inner list members & params + let inner_list_members = inner_list.QueryInterface(Ci.nsISFVInnerList).items; + let inner_list_parameters = inner_list + .QueryInterface(Ci.nsISFVInnerList) + .params.QueryInterface(Ci.nsISFVParams); + Assert.equal(inner_list_members.length, 2, "check inner list length"); + + let inner_item1 = inner_list_members[0].QueryInterface(Ci.nsISFVItem); + Assert.equal( + inner_item1.value.QueryInterface(Ci.nsISFVDecimal).value, + 172.145865, + "check inner list member value" + ); + + let inner_item2 = inner_list_members[1].QueryInterface(Ci.nsISFVItem); + Assert.equal( + inner_item2.value.QueryInterface(Ci.nsISFVByteSeq).value, + "weather", + "check inner list member value" + ); + + Assert.equal( + inner_list_parameters.get("inner_param").QueryInterface(Ci.nsISFVByteSeq) + .value, + "tests", + "get inner list parameter by key and check its value" + ); +}); + +add_task(async function test_sfv_item() { + // create parameters for item + let params = gService.newParameters(); + let param1 = gService.newBool(false); + let param2 = gService.newString("str_value"); + let param3 = gService.newBool(true); + params.set("param_1", param1); + params.set("param_2", param2); + params.set("param_3", param3); + + // create item field + let item = gService.newItem(gService.newToken("*abc"), params); + + Assert.equal( + item.value.QueryInterface(Ci.nsISFVToken).value, + "*abc", + "check items's value" + ); + Assert.equal( + item.params.get("param_1").QueryInterface(Ci.nsISFVBool).value, + false, + "get item parameter by key and check its value" + ); + Assert.equal( + item.params.get("param_2").QueryInterface(Ci.nsISFVString).value, + "str_value", + "get item parameter by key and check its value" + ); + Assert.equal( + item.params.get("param_3").QueryInterface(Ci.nsISFVBool).value, + true, + "get item parameter by key and check its value" + ); + + // check item field serialization + let serialized = item.serialize(); + Assert.equal( + serialized, + `*abc;param_1=?0;param_2="str_value";param_3`, + "serialized output must match expected one" + ); +}); + +add_task(async function test_sfv_list() { + // create primitives for List + let item1_params = gService.newParameters(); + item1_params.set("param_1", gService.newToken("*smth")); + let item1 = gService.newItem(gService.newDecimal(145454.14568), item1_params); + + let item2_params = gService.newParameters(); + item2_params.set("param_1", gService.newBool(true)); + let item2 = gService.newItem( + gService.newByteSequence("weather"), + item2_params + ); + + let inner_list = gService.newInnerList( + [item1, item2], + gService.newParameters() + ); + + // create list field + let list = gService.newList([item1, inner_list]); + + // check list's members + let list_members = list.members; + Assert.equal(list_members.length, 2, "check list length"); + + // check list's member of item type + let member1 = list_members[0].QueryInterface(Ci.nsISFVItem); + Assert.equal( + member1.value.QueryInterface(Ci.nsISFVDecimal).value, + 145454.14568, + "check list member's value" + ); + let member1_parameters = member1.params; + Assert.equal( + member1_parameters.get("param_1").QueryInterface(Ci.nsISFVToken).value, + "*smth", + "get list member's parameter by key and check its value" + ); + + // check list's member of inner list type + let inner_list_members = list_members[1].QueryInterface( + Ci.nsISFVInnerList + ).items; + Assert.equal(inner_list_members.length, 2, "check inner list length"); + + let inner_item1 = inner_list_members[0].QueryInterface(Ci.nsISFVItem); + Assert.equal( + inner_item1.value.QueryInterface(Ci.nsISFVDecimal).value, + 145454.14568, + "check inner list member's value" + ); + + let inner_item2 = inner_list_members[1].QueryInterface(Ci.nsISFVItem); + Assert.equal( + inner_item2.value.QueryInterface(Ci.nsISFVByteSeq).value, + "weather", + "check inner list member's value" + ); + + // check inner list member's params + list_members[1] + .QueryInterface(Ci.nsISFVInnerList) + .params.QueryInterface(Ci.nsISFVParams); + + // check serialization of list field + let expected_serialized = + "145454.146;param_1=*smth, (145454.146;param_1=*smth :d2VhdGhlcg==:;param_1)"; + let actual_serialized = list.serialize(); + Assert.equal( + actual_serialized, + expected_serialized, + "serialized output must match expected one" + ); +}); + +add_task(async function test_sfv_dictionary() { + // create primitives for dictionary field + + // dict member1 + let params1 = gService.newParameters(); + params1.set("mp_1", gService.newBool(true)); + params1.set("mp_2", gService.newDecimal(68.758602)); + let member1 = gService.newItem(gService.newString("member_1"), params1); + + // dict member2 + let params2 = gService.newParameters(); + let inner_item1 = gService.newItem( + gService.newString("inner_item_1"), + gService.newParameters() + ); + let inner_item2 = gService.newItem( + gService.newToken("tok"), + gService.newParameters() + ); + let member2 = gService.newInnerList([inner_item1, inner_item2], params2); + + // dict member3 + let params_3 = gService.newParameters(); + params_3.set("mp_1", gService.newInteger(6586)); + let member3 = gService.newItem(gService.newString("member_3"), params_3); + + // create dictionary field + let dict = gService.newDictionary(); + dict.set("key_1", member1); + dict.set("key_2", member2); + dict.set("key_3", member3); + + // check dictionary keys + let expected = ["key_1", "key_2", "key_3"]; + Assert.deepEqual( + expected, + dict.keys(), + "check dictionary contains all the expected keys" + ); + + // check dictionary members + Assert.throws( + () => { + dict.get("key_4"); + }, + /NS_ERROR_UNEXPECTED/, + "must throw exception as key does not exist in dictionary" + ); + + // let dict_member1 = dict.get("key_1").QueryInterface(Ci.nsISFVItem); + let dict_member2 = dict.get("key_2").QueryInterface(Ci.nsISFVInnerList); + let dict_member3 = dict.get("key_3").QueryInterface(Ci.nsISFVItem); + + // Assert.equal( + // dict_member1.value.QueryInterface(Ci.nsISFVString).value, + // "member_1", + // "check dictionary member's value" + // ); + // Assert.equal( + // dict_member1.params.get("mp_1").QueryInterface(Ci.nsISFVBool).value, + // true, + // "get dictionary member's parameter by key and check its value" + // ); + // Assert.equal( + // dict_member1.params.get("mp_2").QueryInterface(Ci.nsISFVDecimal).value, + // "68.758602", + // "get dictionary member's parameter by key and check its value" + // ); + + let dict_member2_items = dict_member2.QueryInterface( + Ci.nsISFVInnerList + ).items; + let dict_member2_params = dict_member2 + .QueryInterface(Ci.nsISFVInnerList) + .params.QueryInterface(Ci.nsISFVParams); + Assert.equal( + dict_member2_items[0] + .QueryInterface(Ci.nsISFVItem) + .value.QueryInterface(Ci.nsISFVString).value, + "inner_item_1", + "get dictionary member of inner list type, and check inner list member's value" + ); + Assert.equal( + dict_member2_items[1] + .QueryInterface(Ci.nsISFVItem) + .value.QueryInterface(Ci.nsISFVToken).value, + "tok", + "get dictionary member of inner list type, and check inner list member's value" + ); + Assert.throws( + () => { + dict_member2_params.get("some_param"); + }, + /NS_ERROR_UNEXPECTED/, + "must throw exception as dict member's parameters are empty" + ); + + Assert.equal( + dict_member3.value.QueryInterface(Ci.nsISFVString).value, + "member_3", + "check dictionary member's value" + ); + Assert.equal( + dict_member3.params.get("mp_1").QueryInterface(Ci.nsISFVInteger).value, + 6586, + "get dictionary member's parameter by key and check its value" + ); + + // check serialization of Dictionary field + let expected_serialized = `key_1="member_1";mp_1;mp_2=68.759, key_2=("inner_item_1" tok), key_3="member_3";mp_1=6586`; + let actual_serialized = dict.serialize(); + Assert.equal( + actual_serialized, + expected_serialized, + "serialized output must match expected one" + ); + + // check deleting dict member + dict.delete("key_2"); + Assert.deepEqual( + dict.keys(), + ["key_1", "key_3"], + "check that dictionary member has been deleted" + ); + + Assert.throws( + () => { + dict.delete("some_key"); + }, + /NS_ERROR_UNEXPECTED/, + "must throw exception upon attempt to delete by non-existing key" + ); +}); + +add_task(async function test_sfv_item_parsing() { + Assert.ok(gService.parseItem(`"str"`), "input must be parsed into Item"); + Assert.ok(gService.parseItem("12.35;a "), "input must be parsed into Item"); + Assert.ok(gService.parseItem("12.35; a "), "input must be parsed into Item"); + Assert.ok(gService.parseItem("12.35 "), "input must be parsed into Item"); + + Assert.throws( + () => { + gService.parseItem("12.35;\ta "); + }, + /NS_ERROR_FAILURE/, + "item parsing must fail: invalid parameters delimiter" + ); + + Assert.throws( + () => { + gService.parseItem("125666.3565648855455"); + }, + /NS_ERROR_FAILURE/, + "item parsing must fail: decimal too long" + ); +}); + +add_task(async function test_sfv_list_parsing() { + Assert.ok( + gService.parseList( + "(?1;param_1=*smth :d2VhdGhlcg==:;param_1;param_2=145454);inner_param=:d2VpcmR0ZXN0cw==:" + ), + "input must be parsed into List" + ); + Assert.ok("a, (b c)", "input must be parsed into List"); + + Assert.throws(() => { + gService.parseList("?tok", "list parsing must fail"); + }, /NS_ERROR_FAILURE/); + + Assert.throws(() => { + gService.parseList( + "a, (b, c)", + "list parsing must fail: invalid delimiter within inner list" + ); + }, /NS_ERROR_FAILURE/); + + Assert.throws( + () => { + gService.parseList("a, b c"); + }, + /NS_ERROR_FAILURE/, + "list parsing must fail: invalid delimiter" + ); +}); + +add_task(async function test_sfv_dict_parsing() { + Assert.ok( + gService.parseDictionary(`abc=123;a=1;b=2, def=456, ghi=789;q=9;r="+w"`), + "input must be parsed into Dictionary" + ); + Assert.ok( + gService.parseDictionary("a=1\t,\t\t\t c=*"), + "input must be parsed into Dictionary" + ); + Assert.ok( + gService.parseDictionary("a=1\t,\tc=* \t\t"), + "input must be parsed into Dictionary" + ); + + Assert.throws( + () => { + gService.parseDictionary("a=1\t,\tc=*,"); + }, + /NS_ERROR_FAILURE/, + "dictionary parsing must fail: trailing comma" + ); + + Assert.throws( + () => { + gService.parseDictionary("a=1 c=*"); + }, + /NS_ERROR_FAILURE/, + "dictionary parsing must fail: invalid delimiter" + ); + + Assert.throws( + () => { + gService.parseDictionary("INVALID_key=1, c=*"); + }, + /NS_ERROR_FAILURE/, + "dictionary parsing must fail: invalid key format, can't be in uppercase" + ); +}); + +add_task(async function test_sfv_list_parse_serialize() { + let list_field = gService.parseList("1 , 42, (42 43)"); + Assert.equal( + list_field.serialize(), + "1, 42, (42 43)", + "serialized output must match expected one" + ); + + // create new inner list with parameters + function params() { + let inner_list_params = gService.newParameters(); + inner_list_params.set("key1", gService.newString("value1")); + inner_list_params.set("key2", gService.newBool(true)); + inner_list_params.set("key3", gService.newBool(false)); + return inner_list_params; + } + + function changeMembers() { + // set one of list members to inner list and check it's serialized as expected + let members = list_field.members; + members[1] = gService.newInnerList( + [ + gService.newItem( + gService.newDecimal(-1865.75653), + gService.newParameters() + ), + gService.newItem(gService.newToken("token"), gService.newParameters()), + gService.newItem( + gService.newString(`no"yes`), + gService.newParameters() + ), + ], + params() + ); + return members; + } + + list_field.members = changeMembers(); + + Assert.equal( + list_field.serialize(), + `1, (-1865.757 token "no\\"yes");key1="value1";key2;key3=?0, (42 43)`, + "update list member and check list is serialized as expected" + ); +}); + +add_task(async function test_sfv_dict_parse_serialize() { + let dict_field = gService.parseDictionary( + "a=1, b; foo=*, \tc=3, \t \tabc=123;a=1;b=2\t" + ); + Assert.equal( + dict_field.serialize(), + "a=1, b;foo=*, c=3, abc=123;a=1;b=2", + "serialized output must match expected one" + ); + + // set new value for existing dict's key + dict_field.set( + "a", + gService.newItem(gService.newInteger(165), gService.newParameters()) + ); + + // add new member to dict + dict_field.set( + "key", + gService.newItem(gService.newDecimal(45.0), gService.newParameters()) + ); + + // check dict is serialized properly after the above changes + Assert.equal( + dict_field.serialize(), + "a=165, b;foo=*, c=3, abc=123;a=1;b=2, key=45.0", + "update dictionary members and dictionary list is serialized as expected" + ); +}); + +add_task(async function test_sfv_list_parse_more() { + // check parsing of multiline header of List type + let list_field = gService.parseList("(12 abc), 12.456\t\t "); + list_field.parseMore("11, 15, tok"); + Assert.equal( + list_field.serialize(), + "(12 abc), 12.456, 11, 15, tok", + "multi-line field value parsed and serialized successfully" + ); + + // should fail parsing one more line + Assert.throws( + () => { + list_field.parseMore("(tk\t1)"); + }, + /NS_ERROR_FAILURE/, + "line parsing must fail: invalid delimiter in inner list" + ); + Assert.equal( + list_field.serialize(), + "(12 abc), 12.456, 11, 15, tok", + "parsed value must not change if parsing one more line of header fails" + ); +}); + +add_task(async function test_sfv_dict_parse_more() { + // check parsing of multiline header of Dictionary type + let dict_field = gService.parseDictionary(""); + dict_field.parseMore("key2=?0, key3=?1, key4=itm"); + dict_field.parseMore("key1, key5=11, key4=45"); + + Assert.equal( + dict_field.serialize(), + "key2=?0, key3, key4=45, key1, key5=11", + "multi-line field value parsed and serialized successfully" + ); + + // should fail parsing one more line + Assert.throws( + () => { + dict_field.parseMore("c=12, _k=13"); + }, + /NS_ERROR_FAILURE/, + "line parsing must fail: invalid key format" + ); +}); diff --git a/netwerk/test/unit/test_httpauth.js b/netwerk/test/unit/test_httpauth.js new file mode 100644 index 0000000000..9c9de82618 --- /dev/null +++ b/netwerk/test/unit/test_httpauth.js @@ -0,0 +1,204 @@ +/* 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 test makes sure the HTTP authenticated sessions are correctly cleared +// when entering and leaving the private browsing mode. + +"use strict"; + +function run_test() { + var am = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + + const kHost1 = "pbtest3.example.com"; + const kHost2 = "pbtest4.example.com"; + const kPort = 80; + const kHTTP = "http"; + const kBasic = "basic"; + const kRealm = "realm"; + const kDomain = "example.com"; + const kUser = "user"; + const kUser2 = "user2"; + const kPassword = "pass"; + const kPassword2 = "pass2"; + const kEmpty = ""; + + const PRIVATE = true; + const NOT_PRIVATE = false; + + try { + var domain = { value: kEmpty }, + user = { value: kEmpty }, + pass = { value: kEmpty }; + // simulate a login via HTTP auth outside of the private mode + am.setAuthIdentity( + kHTTP, + kHost1, + kPort, + kBasic, + kRealm, + kEmpty, + kDomain, + kUser, + kPassword + ); + // make sure the recently added auth entry is available outside the private browsing mode + am.getAuthIdentity( + kHTTP, + kHost1, + kPort, + kBasic, + kRealm, + kEmpty, + domain, + user, + pass, + NOT_PRIVATE + ); + Assert.equal(domain.value, kDomain); + Assert.equal(user.value, kUser); + Assert.equal(pass.value, kPassword); + + // make sure the added auth entry is no longer accessible in private + domain = { value: kEmpty }; + user = { value: kEmpty }; + pass = { value: kEmpty }; + try { + // should throw + am.getAuthIdentity( + kHTTP, + kHost1, + kPort, + kBasic, + kRealm, + kEmpty, + domain, + user, + pass, + PRIVATE + ); + do_throw( + "Auth entry should not be retrievable after entering the private browsing mode" + ); + } catch (e) { + Assert.equal(domain.value, kEmpty); + Assert.equal(user.value, kEmpty); + Assert.equal(pass.value, kEmpty); + } + + // simulate a login via HTTP auth inside of the private mode + am.setAuthIdentity( + kHTTP, + kHost2, + kPort, + kBasic, + kRealm, + kEmpty, + kDomain, + kUser2, + kPassword2, + PRIVATE + ); + // make sure the recently added auth entry is available inside the private browsing mode + domain = { value: kEmpty }; + user = { value: kEmpty }; + pass = { value: kEmpty }; + am.getAuthIdentity( + kHTTP, + kHost2, + kPort, + kBasic, + kRealm, + kEmpty, + domain, + user, + pass, + PRIVATE + ); + Assert.equal(domain.value, kDomain); + Assert.equal(user.value, kUser2); + Assert.equal(pass.value, kPassword2); + + try { + // make sure the recently added auth entry is not available outside the private browsing mode + domain = { value: kEmpty }; + user = { value: kEmpty }; + pass = { value: kEmpty }; + am.getAuthIdentity( + kHTTP, + kHost2, + kPort, + kBasic, + kRealm, + kEmpty, + domain, + user, + pass, + NOT_PRIVATE + ); + do_throw( + "Auth entry should not be retrievable outside of private browsing mode" + ); + } catch (x) { + Assert.equal(domain.value, kEmpty); + Assert.equal(user.value, kEmpty); + Assert.equal(pass.value, kEmpty); + } + + // simulate leaving private browsing mode + Services.obs.notifyObservers(null, "last-pb-context-exited"); + + // make sure the added auth entry is no longer accessible in any privacy state + domain = { value: kEmpty }; + user = { value: kEmpty }; + pass = { value: kEmpty }; + try { + // should throw (not available in public mode) + am.getAuthIdentity( + kHTTP, + kHost2, + kPort, + kBasic, + kRealm, + kEmpty, + domain, + user, + pass, + NOT_PRIVATE + ); + do_throw( + "Auth entry should not be retrievable after exiting the private browsing mode" + ); + } catch (e) { + Assert.equal(domain.value, kEmpty); + Assert.equal(user.value, kEmpty); + Assert.equal(pass.value, kEmpty); + } + try { + // should throw (no longer available in private mode) + am.getAuthIdentity( + kHTTP, + kHost2, + kPort, + kBasic, + kRealm, + kEmpty, + domain, + user, + pass, + PRIVATE + ); + do_throw( + "Auth entry should not be retrievable in private mode after exiting the private browsing mode" + ); + } catch (x) { + Assert.equal(domain.value, kEmpty); + Assert.equal(user.value, kEmpty); + Assert.equal(pass.value, kEmpty); + } + } catch (e) { + do_throw("Unexpected exception while testing HTTP auth manager: " + e); + } +} diff --git a/netwerk/test/unit/test_httpcancel.js b/netwerk/test/unit/test_httpcancel.js new file mode 100644 index 0000000000..189bcc4392 --- /dev/null +++ b/netwerk/test/unit/test_httpcancel.js @@ -0,0 +1,259 @@ +// This file ensures that canceling a channel early does not +// send the request to the server (bug 350790) +// +// I've also shoehorned in a test that ENSURE_CALLED_BEFORE_CONNECT works as +// expected: see comments that start with ENSURE_CALLED_BEFORE_CONNECT: +// +// This test also checks that cancelling a channel before asyncOpen, after +// onStopRequest, or during onDataAvailable works as expected. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const reason = "testing"; + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +var ios = Services.io; +var ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); +var observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(subject, topic, data) { + subject = subject.QueryInterface(Ci.nsIRequest); + subject.cancelWithReason(Cr.NS_BINDING_ABORTED, reason); + + // ENSURE_CALLED_BEFORE_CONNECT: setting values should still work + try { + subject.QueryInterface(Ci.nsIHttpChannel); + let currentReferrer = subject.getRequestHeader("Referer"); + Assert.equal(currentReferrer, "http://site1.com/"); + var uri = ios.newURI("http://site2.com"); + subject.referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.EMPTY, + true, + uri + ); + } catch (ex) { + do_throw("Exception: " + ex); + } + }, +}; + +let cancelDuringOnStartListener = { + onStartRequest: function test_onStartR(request) { + Assert.equal(request.status, Cr.NS_BINDING_ABORTED); + // We didn't sync the reason to child process. + if (!inChildProcess()) { + Assert.equal(request.canceledReason, reason); + } + + // ENSURE_CALLED_BEFORE_CONNECT: setting referrer should now fail + try { + request.QueryInterface(Ci.nsIHttpChannel); + let currentReferrer = request.getRequestHeader("Referer"); + Assert.equal(currentReferrer, "http://site2.com/"); + var uri = ios.newURI("http://site3.com/"); + + // Need to set NECKO_ERRORS_ARE_FATAL=0 else we'll abort process + Services.env.set("NECKO_ERRORS_ARE_FATAL", "0"); + // we expect setting referrer to fail + try { + request.referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.EMPTY, + true, + uri + ); + do_throw("Error should have been thrown before getting here"); + } catch (ex) {} + } catch (ex) { + do_throw("Exception: " + ex); + } + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + this.resolved(); + }, +}; + +var cancelDuringOnDataListener = { + data: "", + channel: null, + receivedSomeData: null, + onStartRequest: function test_onStartR(request, ctx) { + Assert.equal(request.status, Cr.NS_OK); + }, + + onDataAvailable: function test_ODA(request, stream, offset, count) { + let string = NetUtil.readInputStreamToString(stream, count); + Assert.ok(!string.includes("b")); + this.data += string; + this.channel.cancel(Cr.NS_BINDING_ABORTED); + if (this.receivedSomeData) { + this.receivedSomeData(); + } + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + Assert.ok(this.data.includes("a"), `data: ${this.data}`); + Assert.equal(request.status, Cr.NS_BINDING_ABORTED); + this.resolved(); + }, +}; + +function makeChan(url) { + var chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + + // ENSURE_CALLED_BEFORE_CONNECT: set original value + var uri = ios.newURI("http://site1.com"); + chan.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri); + return chan; +} + +var httpserv = null; + +add_task(async function setup() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/failtest", failtest); + httpserv.registerPathHandler("/cancel_middle", cancel_middle); + httpserv.registerPathHandler("/normal_response", normal_response); + httpserv.start(-1); + + registerCleanupFunction(async () => { + await new Promise(resolve => httpserv.stop(resolve)); + }); +}); + +add_task(async function test_cancel_during_onModifyRequest() { + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/failtest" + ); + + if (!inChildProcess()) { + Services.obs.addObserver(observer, "http-on-modify-request"); + } else { + do_send_remote_message("register-observer"); + await do_await_remote_message("register-observer-done"); + } + + await new Promise(resolve => { + cancelDuringOnStartListener.resolved = resolve; + chan.asyncOpen(cancelDuringOnStartListener); + }); + + if (!inChildProcess()) { + Services.obs.removeObserver(observer, "http-on-modify-request"); + } else { + do_send_remote_message("unregister-observer"); + await do_await_remote_message("unregister-observer-done"); + } +}); + +add_task(async function test_cancel_before_asyncOpen() { + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/failtest" + ); + + chan.cancel(Cr.NS_BINDING_ABORTED); + + Assert.throws( + () => { + chan.asyncOpen(cancelDuringOnStartListener); + }, + /NS_BINDING_ABORTED/, + "cannot open if already cancelled" + ); +}); + +add_task(async function test_cancel_during_onData() { + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/cancel_middle" + ); + + await new Promise(resolve => { + cancelDuringOnDataListener.resolved = resolve; + cancelDuringOnDataListener.channel = chan; + chan.asyncOpen(cancelDuringOnDataListener); + }); +}); + +var cancelAfterOnStopListener = { + data: "", + channel: null, + onStartRequest: function test_onStartR(request, ctx) { + Assert.equal(request.status, Cr.NS_OK); + }, + + onDataAvailable: function test_ODA(request, stream, offset, count) { + let string = NetUtil.readInputStreamToString(stream, count); + this.data += string; + }, + + onStopRequest: function test_onStopR(request, status) { + info("onStopRequest"); + Assert.equal(request.status, Cr.NS_OK); + this.resolved(); + }, +}; + +add_task(async function test_cancel_after_onStop() { + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/normal_response" + ); + + await new Promise(resolve => { + cancelAfterOnStopListener.resolved = resolve; + cancelAfterOnStopListener.channel = chan; + chan.asyncOpen(cancelAfterOnStopListener); + }); + Assert.equal(chan.status, Cr.NS_OK); + + // For now it's unclear if cancelling after onStop should throw, + // silently fail, or overwrite the channel's status as we currently do. + // See discussion in bug 1553083 + chan.cancel(Cr.NS_BINDING_ABORTED); + Assert.equal(chan.status, Cr.NS_BINDING_ABORTED); +}); + +// PATHS + +// /failtest +function failtest(metadata, response) { + do_throw("This should not be reached"); +} + +function cancel_middle(metadata, response) { + response.processAsync(); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + let str1 = "a".repeat(128 * 1024); + response.write(str1, str1.length); + response.bodyOutputStream.flush(); + + let p = new Promise(resolve => { + cancelDuringOnDataListener.receivedSomeData = resolve; + }); + p.then(() => { + let str1 = "b".repeat(128 * 1024); + response.write(str1, str1.length); + response.finish(); + }); +} + +function normal_response(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + let str1 = "Is this normal?"; + response.write(str1, str1.length); +} diff --git a/netwerk/test/unit/test_https_rr_ech_prefs.js b/netwerk/test/unit/test_https_rr_ech_prefs.js new file mode 100644 index 0000000000..28ce808321 --- /dev/null +++ b/netwerk/test/unit/test_https_rr_ech_prefs.js @@ -0,0 +1,535 @@ +/* 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"; + +let trrServer; + +function setup() { + trr_test_setup(); + + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); +} + +setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + Services.prefs.clearUserPref("network.dns.echconfig.enabled"); + Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled"); + Services.prefs.clearUserPref("network.http.http3.enable"); + Services.prefs.clearUserPref("network.http.http2.enabled"); + if (trrServer) { + await trrServer.stop(); + } +}); + +function checkResult(inRecord, noHttp2, noHttp3, result) { + if (!result) { + Assert.throws( + () => { + inRecord + .QueryInterface(Ci.nsIDNSHTTPSSVCRecord) + .GetServiceModeRecord(noHttp2, noHttp3); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should get an error" + ); + return; + } + + let record = inRecord + .QueryInterface(Ci.nsIDNSHTTPSSVCRecord) + .GetServiceModeRecord(noHttp2, noHttp3); + Assert.equal(record.priority, result.expectedPriority); + Assert.equal(record.name, result.expectedName); + Assert.equal(record.selectedAlpn, result.expectedAlpn); +} + +// Test configuration: +// There are two records: one has a echConfig and the other doesn't. +// We want to test if the record with echConfig is preferred. +add_task(async function testEchConfigEnabled() { + let trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", false); + + await trrServer.registerDoHAnswers("test.bar.com", "HTTPS", { + answers: [ + { + name: "test.bar.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.bar_1.com", + values: [{ key: "alpn", value: ["h3-29"] }], + }, + }, + { + name: "test.bar.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.bar_2.com", + values: [ + { key: "alpn", value: ["h2"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + let { inRecord } = await new TRRDNSListener("test.bar.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.bar_1.com", + expectedAlpn: "h3-29", + }); + checkResult(inRecord, false, true, { + expectedPriority: 2, + expectedName: "test.bar_2.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.bar_1.com", + expectedAlpn: "h3-29", + }); + checkResult(inRecord, true, true); + + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + Services.dns.clearCache(true); + + ({ inRecord } = await new TRRDNSListener("test.bar.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + })); + + checkResult(inRecord, false, false, { + expectedPriority: 2, + expectedName: "test.bar_2.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, false, true, { + expectedPriority: 2, + expectedName: "test.bar_2.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.bar_1.com", + expectedAlpn: "h3-29", + }); + checkResult(inRecord, true, true); + + await trrServer.stop(); + trrServer = null; +}); + +// Test configuration: +// There are two records: both have echConfigs, and only one supports h3. +// This test is about testing which record should we get when +// network.dns.http3_echconfig.enabled is true and false. +// When network.dns.http3_echconfig.enabled is false, we should try to +// connect with h2 and echConfig. +add_task(async function testTwoRecordsHaveEchConfig() { + Services.dns.clearCache(true); + + let trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", false); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.foo.com", "HTTPS", { + answers: [ + { + name: "test.foo.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.foo_h3.com", + values: [ + { key: "alpn", value: ["h3"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.foo.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.foo_h2.com", + values: [ + { key: "alpn", value: ["h2"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + let { inRecord } = await new TRRDNSListener("test.foo.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + checkResult(inRecord, false, false, { + expectedPriority: 2, + expectedName: "test.foo_h2.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, false, true, { + expectedPriority: 2, + expectedName: "test.foo_h2.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, true, true); + + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true); + Services.dns.clearCache(true); + ({ inRecord } = await new TRRDNSListener("test.foo.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + })); + + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, false, true, { + expectedPriority: 2, + expectedName: "test.foo_h2.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, true, true); + + await trrServer.stop(); + trrServer = null; +}); + +// Test configuration: +// There are two records: both have echConfigs, and one supports h3 and h2. +// When network.dns.http3_echconfig.enabled is false, we should use the record +// that supports h3 and h2 (the alpn is h2). +add_task(async function testTwoRecordsHaveEchConfig1() { + Services.dns.clearCache(true); + + let trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", false); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.foo.com", "HTTPS", { + answers: [ + { + name: "test.foo.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.foo_h3.com", + values: [ + { key: "alpn", value: ["h3", "h2"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.foo.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.foo_h2.com", + values: [ + { key: "alpn", value: ["h2", "http/1.1"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + let { inRecord } = await new TRRDNSListener("test.foo.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, false, true, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 2, + expectedName: "test.foo_h2.com", + expectedAlpn: "http/1.1", + }); + checkResult(inRecord, true, true, { + expectedPriority: 2, + expectedName: "test.foo_h2.com", + expectedAlpn: "http/1.1", + }); + + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true); + Services.dns.clearCache(true); + ({ inRecord } = await new TRRDNSListener("test.foo.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + })); + + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, false, true, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, true, true, { + expectedPriority: 2, + expectedName: "test.foo_h2.com", + expectedAlpn: "http/1.1", + }); + + await trrServer.stop(); + trrServer = null; +}); + +// Test configuration: +// There are two records: only one support h3 and only one has echConfig. +// This test is about never usng the record without echConfig. +add_task(async function testOneRecordsHasEchConfig() { + Services.dns.clearCache(true); + + let trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", false); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.foo.com", "HTTPS", { + answers: [ + { + name: "test.foo.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.foo_h3.com", + values: [ + { key: "alpn", value: ["h3"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.foo.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.foo_h2.com", + values: [{ key: "alpn", value: ["h2"] }], + }, + }, + ], + }); + + let { inRecord } = await new TRRDNSListener("test.foo.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, false, true, { + expectedPriority: 2, + expectedName: "test.foo_h2.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, true, true); + + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true); + Services.dns.clearCache(true); + ({ inRecord } = await new TRRDNSListener("test.foo.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + })); + + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, false, true, { + expectedPriority: 2, + expectedName: "test.foo_h2.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, true, true); + + await trrServer.stop(); + trrServer = null; +}); + +// Test the case that "network.http.http3.enable" and +// "network.http.http2.enabled" are true/false. +add_task(async function testHttp3AndHttp2Pref() { + Services.dns.clearCache(true); + + let trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setBoolPref("network.http.http3.enable", false); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", false); + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", false); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.foo.com", "HTTPS", { + answers: [ + { + name: "test.foo.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.foo_h3.com", + values: [ + { key: "alpn", value: ["h3", "h2"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.foo.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.foo_h2.com", + values: [ + { key: "alpn", value: ["h2"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + let { inRecord } = await new TRRDNSListener("test.foo.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, false, true, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false); + checkResult(inRecord, true, true); + + Services.prefs.setBoolPref("network.http.http2.enabled", false); + checkResult(inRecord, false, false); + + Services.prefs.setBoolPref("network.http.http3.enable", true); + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, false, true); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.foo_h3.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, true, true); + + await trrServer.stop(); + trrServer = null; +}); diff --git a/netwerk/test/unit/test_https_rr_sorted_alpn.js b/netwerk/test/unit/test_https_rr_sorted_alpn.js new file mode 100644 index 0000000000..d2f9e9344b --- /dev/null +++ b/netwerk/test/unit/test_https_rr_sorted_alpn.js @@ -0,0 +1,226 @@ +/* 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"; + +let trrServer; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +add_setup(async function setup() { + trr_test_setup(); + registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.http.http3.support_version1"); + Services.prefs.clearUserPref("security.tls.version.max"); + if (trrServer) { + await trrServer.stop(); + } + }); + + if (mozinfo.socketprocess_networking) { + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } +}); + +function checkResult(inRecord, noHttp2, noHttp3, result) { + if (!result) { + Assert.throws( + () => { + inRecord + .QueryInterface(Ci.nsIDNSHTTPSSVCRecord) + .GetServiceModeRecord(noHttp2, noHttp3); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should get an error" + ); + return; + } + + let record = inRecord + .QueryInterface(Ci.nsIDNSHTTPSSVCRecord) + .GetServiceModeRecord(noHttp2, noHttp3); + Assert.equal(record.priority, result.expectedPriority); + Assert.equal(record.name, result.expectedName); + Assert.equal(record.selectedAlpn, result.expectedAlpn); +} + +add_task(async function testSortedAlpnH3() { + Services.dns.clearCache(true); + + trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.support_version1", true); + await trrServer.registerDoHAnswers("test.alpn.com", "HTTPS", { + answers: [ + { + name: "test.alpn.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.alpn.com", + values: [{ key: "alpn", value: ["h2", "http/1.1", "h3-30", "h3"] }], + }, + }, + ], + }); + + let { inRecord } = await new TRRDNSListener("test.alpn.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, false, true, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, true, true, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "http/1.1", + }); + + Services.prefs.setBoolPref("network.http.http3.support_version1", false); + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "h3-30", + }); + checkResult(inRecord, false, true, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "h3-30", + }); + checkResult(inRecord, true, true, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "http/1.1", + }); + Services.prefs.setBoolPref("network.http.http3.support_version1", true); + + // Disable TLS1.3 + Services.prefs.setIntPref("security.tls.version.max", 3); + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, false, true, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "http/1.1", + }); + checkResult(inRecord, true, true, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "http/1.1", + }); + + // Enable TLS1.3 + Services.prefs.setIntPref("security.tls.version.max", 4); + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, false, true, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "h3", + }); + checkResult(inRecord, true, true, { + expectedPriority: 1, + expectedName: "test.alpn.com", + expectedAlpn: "http/1.1", + }); +}); + +add_task(async function testSortedAlpnH2() { + Services.dns.clearCache(true); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + await trrServer.registerDoHAnswers("test.alpn_2.com", "HTTPS", { + answers: [ + { + name: "test.alpn_2.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.alpn_2.com", + values: [{ key: "alpn", value: ["http/1.1", "h2"] }], + }, + }, + ], + }); + + let { inRecord } = await new TRRDNSListener("test.alpn_2.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + checkResult(inRecord, false, false, { + expectedPriority: 1, + expectedName: "test.alpn_2.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, false, true, { + expectedPriority: 1, + expectedName: "test.alpn_2.com", + expectedAlpn: "h2", + }); + checkResult(inRecord, true, false, { + expectedPriority: 1, + expectedName: "test.alpn_2.com", + expectedAlpn: "http/1.1", + }); + checkResult(inRecord, true, true, { + expectedPriority: 1, + expectedName: "test.alpn_2.com", + expectedAlpn: "http/1.1", + }); + + await trrServer.stop(); + trrServer = null; +}); diff --git a/netwerk/test/unit/test_httpssvc_ech_with_alpn.js b/netwerk/test/unit/test_httpssvc_ech_with_alpn.js new file mode 100644 index 0000000000..bd41eec964 --- /dev/null +++ b/netwerk/test/unit/test_httpssvc_ech_with_alpn.js @@ -0,0 +1,246 @@ +/* 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"; + +let trrServer; + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +add_setup(async function setup() { + // Allow telemetry probes which may otherwise be disabled for some + // applications (e.g. Thunderbird). + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + trr_test_setup(); + + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", false); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY); + + // Set the server to always select http/1.1 + Services.env.set("MOZ_TLS_ECH_ALPN_FLAG", 1); + + await asyncStartTLSTestServer( + "EncryptedClientHelloServer", + "../../../security/manager/ssl/tests/unit/test_encrypted_client_hello" + ); +}); + +registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("network.trr.uri"); + Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + Services.prefs.clearUserPref("network.dns.echconfig.enabled"); + Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled"); + Services.prefs.clearUserPref( + "network.dns.echconfig.fallback_to_origin_when_all_failed" + ); + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); + Services.prefs.clearUserPref("network.dns.port_prefixed_qname_https_rr"); + Services.env.set("MOZ_TLS_ECH_ALPN_FLAG", ""); + if (trrServer) { + await trrServer.stop(); + } +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + resolve([req, buffer]); + } + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +function ActivityObserver() {} + +ActivityObserver.prototype = { + activites: [], + observeConnectionActivity( + aHost, + aPort, + aSSL, + aHasECH, + aIsHttp3, + aActivityType, + aActivitySubtype, + aTimestamp, + aExtraStringData + ) { + dump( + "*** Connection Activity 0x" + + aActivityType.toString(16) + + " 0x" + + aActivitySubtype.toString(16) + + " " + + aExtraStringData + + "\n" + ); + this.activites.push({ host: aHost, subType: aActivitySubtype }); + }, +}; + +function checkHttpActivities(activites) { + let foundDNSAndSocket = false; + let foundSettingECH = false; + let foundConnectionCreated = false; + for (let activity of activites) { + switch (activity.subType) { + case Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_DNSANDSOCKET_CREATED: + case Ci.nsIHttpActivityObserver + .ACTIVITY_SUBTYPE_SPECULATIVE_DNSANDSOCKET_CREATED: + foundDNSAndSocket = true; + break; + case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_ECH_SET: + foundSettingECH = true; + break; + case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_CONNECTION_CREATED: + foundConnectionCreated = true; + break; + default: + break; + } + } + + Assert.equal(foundDNSAndSocket, true, "Should have one DnsAndSock created"); + Assert.equal(foundSettingECH, true, "Should have echConfig"); + Assert.equal( + foundConnectionCreated, + true, + "Should have one connection created" + ); +} + +async function testWrapper(alpnAdvertisement) { + const ECH_CONFIG_FIXED = + "AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAEAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA"; + trrServer = new TRRServer(); + await trrServer.start(); + + let observerService = Cc[ + "@mozilla.org/network/http-activity-distributor;1" + ].getService(Ci.nsIHttpActivityDistributor); + let observer = new ActivityObserver(); + observerService.addObserver(observer); + observerService.observeConnection = true; + + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + // Only the last record is valid to use. + await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", { + answers: [ + { + name: "ech-private.example.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "ech-private.example.com", + values: [ + { key: "alpn", value: alpnAdvertisement }, + { key: "port", value: 8443 }, + { + key: "echconfig", + value: ECH_CONFIG_FIXED, + needBase64Decode: true, + }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("ech-private.example.com", "A", { + answers: [ + { + name: "ech-private.example.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await new TRRDNSListener("ech-private.example.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + HandshakeTelemetryHelpers.resetHistograms(); + let chan = makeChan(`https://ech-private.example.com`); + await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + let securityInfo = chan.securityInfo; + Assert.ok(securityInfo.isAcceptedEch, "This host should have accepted ECH"); + + // Only check telemetry if network process is disabled. + if (!mozinfo.socketprocess_networking) { + HandshakeTelemetryHelpers.checkSuccess(["", "_ECH", "_FIRST_TRY"]); + HandshakeTelemetryHelpers.checkEmpty(["_CONSERVATIVE", "_ECH_GREASE"]); + } + + await trrServer.stop(); + observerService.removeObserver(observer); + observerService.observeConnection = false; + + let filtered = observer.activites.filter( + activity => activity.host === "ech-private.example.com" + ); + checkHttpActivities(filtered); +} + +add_task(async function h1Advertised() { + await testWrapper(["http/1.1"]); +}); + +add_task(async function h2Advertised() { + await testWrapper(["h2"]); +}); + +add_task(async function h3Advertised() { + await testWrapper(["h3"]); +}); + +add_task(async function h1h2Advertised() { + await testWrapper(["http/1.1", "h2"]); +}); + +add_task(async function h2h3Advertised() { + await testWrapper(["h3", "h2"]); +}); + +add_task(async function unknownAdvertised() { + await testWrapper(["foo"]); +}); diff --git a/netwerk/test/unit/test_httpssvc_https_upgrade.js b/netwerk/test/unit/test_httpssvc_https_upgrade.js new file mode 100644 index 0000000000..51b711d983 --- /dev/null +++ b/netwerk/test/unit/test_httpssvc_https_upgrade.js @@ -0,0 +1,350 @@ +/* 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 certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +add_setup(async function setup() { + trr_test_setup(); + + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc" + ); + + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + + Services.prefs.setBoolPref( + "network.dns.use_https_rr_for_speculative_connection", + true + ); + + registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + Services.prefs.clearUserPref( + "network.dns.use_https_rr_for_speculative_connection" + ); + Services.prefs.clearUserPref("network.dns.notifyResolution"); + Services.prefs.clearUserPref("network.dns.disablePrefetch"); + }); + + if (mozinfo.socketprocess_networking) { + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } + + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +// When observer is specified, the channel will be suspended when receiving +// "http-on-modify-request". +function channelOpenPromise(chan, flags, observer) { + return new Promise(resolve => { + function finish(req, buffer) { + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + resolve([req, buffer]); + } + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + if (observer) { + let topic = "http-on-modify-request"; + Services.obs.addObserver(observer, topic); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +class EventSinkListener { + getInterface(iid) { + if (iid.equals(Ci.nsIChannelEventSink)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } + asyncOnChannelRedirect(oldChan, newChan, flags, callback) { + Assert.equal(oldChan.URI.hostPort, newChan.URI.hostPort); + Assert.equal(oldChan.URI.scheme, "http"); + Assert.equal(newChan.URI.scheme, "https"); + callback.onRedirectVerifyCallback(Cr.NS_OK); + } +} + +EventSinkListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIChannelEventSink", +]); + +// Test if the request is upgraded to https with a HTTPSSVC record. +add_task(async function testUseHTTPSSVCAsHSTS() { + Services.dns.clearCache(true); + // Do DNS resolution before creating the channel, so the HTTPSSVC record will + // be resolved from the cache. + await new TRRDNSListener("test.httpssvc.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + // Since the HTTPS RR should be served from cache, the DNS record is available + // before nsHttpChannel::MaybeUseHTTPSRRForUpgrade() is called. + let chan = makeChan(`http://test.httpssvc.com:80/server-timing`); + let listener = new EventSinkListener(); + chan.notificationCallbacks = listener; + + let [req] = await channelOpenPromise(chan); + + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + chan = makeChan(`http://test.httpssvc.com:80/server-timing`); + listener = new EventSinkListener(); + chan.notificationCallbacks = listener; + + [req] = await channelOpenPromise(chan); + + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); +}); + +// Test the case that we got an invalid DNS response. In this case, +// nsHttpChannel::OnHTTPSRRAvailable is called after +// nsHttpChannel::MaybeUseHTTPSRRForUpgrade. +add_task(async function testInvalidDNSResult() { + Services.dns.clearCache(true); + + let httpserv = new HttpServer(); + let content = "ok"; + httpserv.registerPathHandler("/", function handler(metadata, response) { + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + }); + httpserv.start(-1); + httpserv.identity.setPrimary( + "http", + "foo.notexisted.com", + httpserv.identity.primaryPort + ); + + let chan = makeChan( + `http://foo.notexisted.com:${httpserv.identity.primaryPort}/` + ); + let [, response] = await channelOpenPromise(chan); + Assert.equal(response, content); + await new Promise(resolve => httpserv.stop(resolve)); +}); + +// The same test as above, but nsHttpChannel::MaybeUseHTTPSRRForUpgrade is +// called after nsHttpChannel::OnHTTPSRRAvailable. +add_task(async function testInvalidDNSResult1() { + Services.dns.clearCache(true); + + let httpserv = new HttpServer(); + let content = "ok"; + httpserv.registerPathHandler("/", function handler(metadata, response) { + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + }); + httpserv.start(-1); + httpserv.identity.setPrimary( + "http", + "foo.notexisted.com", + httpserv.identity.primaryPort + ); + + let chan = makeChan( + `http://foo.notexisted.com:${httpserv.identity.primaryPort}/` + ); + + let topic = "http-on-modify-request"; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == topic) { + Services.obs.removeObserver(observer, topic); + let channel = aSubject.QueryInterface(Ci.nsIChannel); + channel.suspend(); + + new TRRDNSListener("foo.notexisted.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + expectedSuccess: false, + }).then(() => channel.resume()); + } + }, + }; + + let [, response] = await channelOpenPromise(chan, 0, observer); + Assert.equal(response, content); + await new Promise(resolve => httpserv.stop(resolve)); +}); + +add_task(async function testLiteralIP() { + let httpserv = new HttpServer(); + let content = "ok"; + httpserv.registerPathHandler("/", function handler(metadata, response) { + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + }); + httpserv.start(-1); + + let chan = makeChan(`http://127.0.0.1:${httpserv.identity.primaryPort}/`); + let [, response] = await channelOpenPromise(chan); + Assert.equal(response, content); + await new Promise(resolve => httpserv.stop(resolve)); +}); + +// Test the case that an HTTPS RR is available and the server returns a 307 +// for redirecting back to http. +add_task(async function testEndlessUpgradeDowngrade() { + Services.dns.clearCache(true); + + let httpserv = new HttpServer(); + let content = "okok"; + httpserv.start(-1); + let port = httpserv.identity.primaryPort; + httpserv.registerPathHandler( + `/redirect_to_http`, + function handler(metadata, response) { + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + } + ); + httpserv.identity.setPrimary("http", "test.httpsrr.redirect.com", port); + + let chan = makeChan( + `http://test.httpsrr.redirect.com:${port}/redirect_to_http?port=${port}` + ); + + let [, response] = await channelOpenPromise(chan); + Assert.equal(response, content); + await new Promise(resolve => httpserv.stop(resolve)); +}); + +add_task(async function testHttpRequestBlocked() { + Services.dns.clearCache(true); + + let dnsRequestObserver = { + register() { + this.obs = Services.obs; + this.obs.addObserver(this, "dns-resolution-request"); + }, + unregister() { + if (this.obs) { + this.obs.removeObserver(this, "dns-resolution-request"); + } + }, + observe(subject, topic, data) { + if (topic == "dns-resolution-request") { + Assert.ok(false, "unreachable"); + } + }, + }; + + dnsRequestObserver.register(); + Services.prefs.setBoolPref("network.dns.notifyResolution", true); + Services.prefs.setBoolPref("network.dns.disablePrefetch", true); + + let httpserv = new HttpServer(); + httpserv.registerPathHandler("/", function handler(metadata, response) { + Assert.ok(false, "unreachable"); + }); + httpserv.start(-1); + httpserv.identity.setPrimary( + "http", + "foo.blocked.com", + httpserv.identity.primaryPort + ); + + let chan = makeChan( + `http://foo.blocked.com:${httpserv.identity.primaryPort}/` + ); + + let topic = "http-on-modify-request"; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == topic) { + Services.obs.removeObserver(observer, topic); + let channel = aSubject.QueryInterface(Ci.nsIChannel); + channel.cancel(Cr.NS_BINDING_ABORTED); + } + }, + }; + + let [request] = await channelOpenPromise(chan, CL_EXPECT_FAILURE, observer); + request.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(request.status, Cr.NS_BINDING_ABORTED); + dnsRequestObserver.unregister(); + await new Promise(resolve => httpserv.stop(resolve)); +}); + +function createPrincipal(url) { + return Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(url), + {} + ); +} + +// Test if the Origin header stays the same after an internal HTTPS upgrade +// caused by HTTPS RR. +add_task(async function testHTTPSRRUpgradeWithOriginHeader() { + Services.dns.clearCache(true); + + const url = "http://test.httpssvc.com:80/origin_header"; + const originURL = "http://example.com"; + let chan = Services.io + .newChannelFromURIWithProxyFlags( + Services.io.newURI(url), + null, + Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL, + null, + createPrincipal(originURL), + createPrincipal(url), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_DOCUMENT + ) + .QueryInterface(Ci.nsIHttpChannel); + chan.referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.EMPTY, + true, + NetUtil.newURI(url) + ); + chan.setRequestHeader("Origin", originURL, false); + + let [req, buf] = await channelOpenPromise(chan); + + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + Assert.equal(buf, originURL); +}); diff --git a/netwerk/test/unit/test_httpssvc_iphint.js b/netwerk/test/unit/test_httpssvc_iphint.js new file mode 100644 index 0000000000..ffa73167e2 --- /dev/null +++ b/netwerk/test/unit/test_httpssvc_iphint.js @@ -0,0 +1,309 @@ +/* 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"; + +let h2Port; +let trrServer; + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +add_setup(async function setup() { + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + trr_test_setup(); + + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + + registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + Services.prefs.clearUserPref("network.dns.disablePrefetch"); + await trrServer.stop(); + }); + + if (mozinfo.socketprocess_networking) { + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } +}); + +// Test if IP hint addresses can be accessed as regular A/AAAA records. +add_task(async function testStoreIPHint() { + trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.IPHint.com", "HTTPS", { + answers: [ + { + name: "test.IPHint.com", + ttl: 999, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.IPHint.com", + values: [ + { key: "alpn", value: ["h2", "h3"] }, + { key: "port", value: 8888 }, + { key: "ipv4hint", value: ["1.2.3.4", "5.6.7.8"] }, + { key: "ipv6hint", value: ["::1", "fe80::794f:6d2c:3d5e:7836"] }, + ], + }, + }, + ], + }); + + let { inRecord } = await new TRRDNSListener("test.IPHint.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).ttl, 999); + Assert.equal(answer[0].priority, 1); + Assert.equal(answer[0].name, "test.IPHint.com"); + Assert.equal(answer[0].values.length, 4); + Assert.deepEqual( + answer[0].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn, + ["h2", "h3"], + "got correct answer" + ); + Assert.equal( + answer[0].values[1].QueryInterface(Ci.nsISVCParamPort).port, + 8888, + "got correct answer" + ); + Assert.equal( + answer[0].values[2].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0] + .address, + "1.2.3.4", + "got correct answer" + ); + Assert.equal( + answer[0].values[2].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[1] + .address, + "5.6.7.8", + "got correct answer" + ); + Assert.equal( + answer[0].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0] + .address, + "::1", + "got correct answer" + ); + Assert.equal( + answer[0].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[1] + .address, + "fe80::794f:6d2c:3d5e:7836", + "got correct answer" + ); + + async function verifyAnswer(flags, expectedAddresses) { + // eslint-disable-next-line no-shadow + let { inRecord } = await new TRRDNSListener("test.IPHint.com", { + flags, + expectedSuccess: false, + }); + Assert.ok(inRecord); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + let addresses = []; + while (inRecord.hasMore()) { + addresses.push(inRecord.getNextAddrAsString()); + } + Assert.deepEqual(addresses, expectedAddresses); + Assert.equal(inRecord.ttl, 999); + } + + await verifyAnswer(Ci.nsIDNSService.RESOLVE_IP_HINT, [ + "1.2.3.4", + "5.6.7.8", + "::1", + "fe80::794f:6d2c:3d5e:7836", + ]); + + await verifyAnswer( + Ci.nsIDNSService.RESOLVE_IP_HINT | Ci.nsIDNSService.RESOLVE_DISABLE_IPV4, + ["::1", "fe80::794f:6d2c:3d5e:7836"] + ); + + await verifyAnswer( + Ci.nsIDNSService.RESOLVE_IP_HINT | Ci.nsIDNSService.RESOLVE_DISABLE_IPV6, + ["1.2.3.4", "5.6.7.8"] + ); + + await trrServer.stop(); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +// Test if we can connect to the server with the IP hint address. +add_task(async function testConnectionWithIPHint() { + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + "https://127.0.0.1:" + h2Port + "/httpssvc_use_iphint" + ); + + // Resolving test.iphint.com should be failed. + let { inStatus } = await new TRRDNSListener("test.iphint.com", { + expectedSuccess: false, + }); + Assert.equal( + inStatus, + Cr.NS_ERROR_UNKNOWN_HOST, + "status is NS_ERROR_UNKNOWN_HOST" + ); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + // The connection should be succeeded since the IP hint is 127.0.0.1. + let chan = makeChan(`http://test.iphint.com:8080/server-timing`); + // Note that the partitionKey stored in DNS cache would be + // "%28https%2Ciphint.com%29". The http request to test.iphint.com will be + // upgraded to https and the ip hint address will be used by the https + // request in the end. + let [req] = await channelOpenPromise(chan); + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + + await trrServer.stop(); +}); + +// Test the case that we failed to use IP Hint address because DNS cache +// is bypassed. +add_task(async function testIPHintWithFreshDNS() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + // To make sure NS_HTTP_REFRESH_DNS not be cleared. + Services.prefs.setBoolPref("network.dns.disablePrefetch", true); + + await trrServer.registerDoHAnswers("test.iphint.org", "HTTPS", { + answers: [ + { + name: "test.iphint.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 0, + name: "svc.iphint.net", + values: [], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("svc.iphint.net", "HTTPS", { + answers: [ + { + name: "svc.iphint.net", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "svc.iphint.net", + values: [ + { key: "alpn", value: "h2" }, + { key: "port", value: h2Port }, + { key: "ipv4hint", value: "127.0.0.1" }, + ], + }, + }, + ], + }); + + let { inRecord } = await new TRRDNSListener("test.iphint.org", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer[0].priority, 1); + Assert.equal(answer[0].name, "svc.iphint.net"); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + let chan = makeChan(`https://test.iphint.org/server-timing`); + chan.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + let [req] = await channelOpenPromise( + chan, + CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL + ); + // Failed because there no A record for "svc.iphint.net". + Assert.equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST); + + await trrServer.registerDoHAnswers("svc.iphint.net", "A", { + answers: [ + { + name: "svc.iphint.net", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + chan = makeChan(`https://test.iphint.org/server-timing`); + chan.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_httpssvc_priority.js b/netwerk/test/unit/test_httpssvc_priority.js new file mode 100644 index 0000000000..ef9f8286a1 --- /dev/null +++ b/netwerk/test/unit/test_httpssvc_priority.js @@ -0,0 +1,125 @@ +/* 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"; + +trr_test_setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.dns.echconfig.enabled"); +}); + +add_task(async function testPriorityAndECHConfig() { + let trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + + Services.prefs.setBoolPref("network.dns.echconfig.enabled", false); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.priority.com", "HTTPS", { + answers: [ + { + name: "test.priority.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.p1.com", + values: [{ key: "alpn", value: ["h2", "h3"] }], + }, + }, + { + name: "test.priority.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 4, + name: "test.p4.com", + values: [{ key: "echconfig", value: "456..." }], + }, + }, + { + name: "test.priority.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 3, + name: "test.p3.com", + values: [{ key: "ipv4hint", value: "1.2.3.4" }], + }, + }, + { + name: "test.priority.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.p2.com", + values: [{ key: "echconfig", value: "123..." }], + }, + }, + ], + }); + + let { inRecord } = await new TRRDNSListener("test.priority.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer.length, 4); + + Assert.equal(answer[0].priority, 1); + Assert.equal(answer[0].name, "test.p1.com"); + + Assert.equal(answer[1].priority, 2); + Assert.equal(answer[1].name, "test.p2.com"); + + Assert.equal(answer[2].priority, 3); + Assert.equal(answer[2].name, "test.p3.com"); + + Assert.equal(answer[3].priority, 4); + Assert.equal(answer[3].name, "test.p4.com"); + + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + Services.dns.clearCache(true); + ({ inRecord } = await new TRRDNSListener("test.priority.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + })); + + answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer.length, 4); + + Assert.equal(answer[0].priority, 2); + Assert.equal(answer[0].name, "test.p2.com"); + Assert.equal( + answer[0].values[0].QueryInterface(Ci.nsISVCParamEchConfig).echconfig, + "123...", + "got correct answer" + ); + + Assert.equal(answer[1].priority, 4); + Assert.equal(answer[1].name, "test.p4.com"); + Assert.equal( + answer[1].values[0].QueryInterface(Ci.nsISVCParamEchConfig).echconfig, + "456...", + "got correct answer" + ); + + Assert.equal(answer[2].priority, 1); + Assert.equal(answer[2].name, "test.p1.com"); + + Assert.equal(answer[3].priority, 3); + Assert.equal(answer[3].name, "test.p3.com"); +}); diff --git a/netwerk/test/unit/test_httpssvc_retry_with_ech.js b/netwerk/test/unit/test_httpssvc_retry_with_ech.js new file mode 100644 index 0000000000..44bed59772 --- /dev/null +++ b/netwerk/test/unit/test_httpssvc_retry_with_ech.js @@ -0,0 +1,511 @@ +/* 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"; + +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +let trrServer; +let h3Port; +let h3EchConfig; + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +function checkSecurityInfo(chan, expectPrivateDNS, expectAcceptedECH) { + let securityInfo = chan.securityInfo; + Assert.equal( + securityInfo.isAcceptedEch, + expectAcceptedECH, + "ECH Status == Expected Status" + ); + Assert.equal( + securityInfo.usedPrivateDNS, + expectPrivateDNS, + "Private DNS Status == Expected Status" + ); +} + +add_setup(async function setup() { + // Allow telemetry probes which may otherwise be disabled for some + // applications (e.g. Thunderbird). + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + trr_test_setup(); + + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY); + + await asyncStartTLSTestServer( + "EncryptedClientHelloServer", + "../../../security/manager/ssl/tests/unit/test_encrypted_client_hello" + ); + + h3Port = Services.env.get("MOZHTTP3_PORT_ECH"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + h3EchConfig = Services.env.get("MOZHTTP3_ECH"); + Assert.notEqual(h3EchConfig, null); + Assert.notEqual(h3EchConfig, ""); +}); + +registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("network.trr.uri"); + Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + Services.prefs.clearUserPref("network.dns.echconfig.enabled"); + Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled"); + Services.prefs.clearUserPref( + "network.dns.echconfig.fallback_to_origin_when_all_failed" + ); + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); + Services.prefs.clearUserPref("network.dns.port_prefixed_qname_https_rr"); + Services.prefs.clearUserPref("security.tls.ech.grease_http3"); + Services.prefs.clearUserPref("security.tls.ech.grease_probability"); + if (trrServer) { + await trrServer.stop(); + } +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + resolve([req, buffer]); + } + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +function ActivityObserver() {} + +ActivityObserver.prototype = { + activites: [], + observeConnectionActivity( + aHost, + aPort, + aSSL, + aHasECH, + aIsHttp3, + aActivityType, + aActivitySubtype, + aTimestamp, + aExtraStringData + ) { + dump( + "*** Connection Activity 0x" + + aActivityType.toString(16) + + " 0x" + + aActivitySubtype.toString(16) + + " " + + aExtraStringData + + "\n" + ); + this.activites.push({ host: aHost, subType: aActivitySubtype }); + }, +}; + +function checkHttpActivities(activites, expectECH) { + let foundDNSAndSocket = false; + let foundSettingECH = false; + let foundConnectionCreated = false; + for (let activity of activites) { + switch (activity.subType) { + case Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_DNSANDSOCKET_CREATED: + case Ci.nsIHttpActivityObserver + .ACTIVITY_SUBTYPE_SPECULATIVE_DNSANDSOCKET_CREATED: + foundDNSAndSocket = true; + break; + case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_ECH_SET: + foundSettingECH = true; + break; + case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_CONNECTION_CREATED: + foundConnectionCreated = true; + break; + default: + break; + } + } + + Assert.equal(foundDNSAndSocket, true, "Should have one DnsAndSock created"); + Assert.equal(foundSettingECH, expectECH, "Should have echConfig"); + Assert.equal( + foundConnectionCreated, + true, + "Should have one connection created" + ); +} + +add_task(async function testConnectWithECH() { + const ECH_CONFIG_FIXED = + "AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAEAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA"; + trrServer = new TRRServer(); + await trrServer.start(); + + let observerService = Cc[ + "@mozilla.org/network/http-activity-distributor;1" + ].getService(Ci.nsIHttpActivityDistributor); + let observer = new ActivityObserver(); + observerService.addObserver(observer); + observerService.observeConnection = true; + + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + // Only the last record is valid to use. + await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", { + answers: [ + { + name: "ech-private.example.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "ech-private.example.com", + values: [ + { key: "alpn", value: "http/1.1" }, + { key: "port", value: 8443 }, + { + key: "echconfig", + value: ECH_CONFIG_FIXED, + needBase64Decode: true, + }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("ech-private.example.com", "A", { + answers: [ + { + name: "ech-private.example.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await new TRRDNSListener("ech-private.example.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + HandshakeTelemetryHelpers.resetHistograms(); + let chan = makeChan(`https://ech-private.example.com`); + await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + checkSecurityInfo(chan, true, true); + // Only check telemetry if network process is disabled. + if (!mozinfo.socketprocess_networking) { + HandshakeTelemetryHelpers.checkSuccess(["", "_ECH", "_FIRST_TRY"]); + HandshakeTelemetryHelpers.checkEmpty(["_CONSERVATIVE", "_ECH_GREASE"]); + } + + await trrServer.stop(); + observerService.removeObserver(observer); + observerService.observeConnection = false; + + let filtered = observer.activites.filter( + activity => activity.host === "ech-private.example.com" + ); + checkHttpActivities(filtered, true); +}); + +add_task(async function testEchRetry() { + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + Services.dns.clearCache(true); + + const ECH_CONFIG_TRUSTED_RETRY = + "AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAMAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA"; + trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + // Only the last record is valid to use. + await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", { + answers: [ + { + name: "ech-private.example.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "ech-private.example.com", + values: [ + { key: "alpn", value: "http/1.1" }, + { key: "port", value: 8443 }, + { + key: "echconfig", + value: ECH_CONFIG_TRUSTED_RETRY, + needBase64Decode: true, + }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("ech-private.example.com", "A", { + answers: [ + { + name: "ech-private.example.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await new TRRDNSListener("ech-private.example.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + + HandshakeTelemetryHelpers.resetHistograms(); + let chan = makeChan(`https://ech-private.example.com`); + await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + checkSecurityInfo(chan, true, true); + // Only check telemetry if network process is disabled. + if (!mozinfo.socketprocess_networking) { + for (let hName of ["SSL_HANDSHAKE_RESULT", "SSL_HANDSHAKE_RESULT_ECH"]) { + let h = Services.telemetry.getHistogramById(hName); + HandshakeTelemetryHelpers.assertHistogramMap( + h.snapshot(), + new Map([ + ["0", 1], + ["188", 1], + ]) + ); + } + HandshakeTelemetryHelpers.checkEntry(["_FIRST_TRY"], 188, 1); + HandshakeTelemetryHelpers.checkEmpty(["_CONSERVATIVE", "_ECH_GREASE"]); + } + + await trrServer.stop(); +}); + +async function H3ECHTest( + echConfig, + expectedHistKey, + expectedHistEntries, + advertiseECH +) { + Services.dns.clearCache(true); + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + await new Promise(resolve => setTimeout(resolve, 1000)); + resetEchTelemetry(); + trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.dns.port_prefixed_qname_https_rr", true); + + let observerService = Cc[ + "@mozilla.org/network/http-activity-distributor;1" + ].getService(Ci.nsIHttpActivityDistributor); + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + let observer = new ActivityObserver(); + observerService.addObserver(observer); + observerService.observeConnection = true; + // Clear activities for past connections + observer.activites = []; + + let portPrefixedName = `_${h3Port}._https.public.example.com`; + let vals = [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + ]; + if (advertiseECH) { + vals.push({ + key: "echconfig", + value: echConfig, + needBase64Decode: true, + }); + } + // Only the last record is valid to use. + + await trrServer.registerDoHAnswers(portPrefixedName, "HTTPS", { + answers: [ + { + name: portPrefixedName, + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: ".", + values: vals, + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("public.example.com", "A", { + answers: [ + { + name: "public.example.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await new TRRDNSListener("public.example.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + port: h3Port, + }); + + let chan = makeChan(`https://public.example.com:${h3Port}`); + let [req] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.protocolVersion, "h3-29"); + checkSecurityInfo(chan, true, advertiseECH); + + await trrServer.stop(); + + observerService.removeObserver(observer); + observerService.observeConnection = false; + + let filtered = observer.activites.filter( + activity => activity.host === "public.example.com" + ); + checkHttpActivities(filtered, advertiseECH); + await checkEchTelemetry(expectedHistKey, expectedHistEntries); +} + +function resetEchTelemetry() { + Services.telemetry.getKeyedHistogramById("HTTP3_ECH_OUTCOME").clear(); +} + +async function checkEchTelemetry(histKey, histEntries) { + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + await new Promise(resolve => setTimeout(resolve, 1000)); + let values = Services.telemetry + .getKeyedHistogramById("HTTP3_ECH_OUTCOME") + .snapshot()[histKey]; + if (!mozinfo.socketprocess_networking) { + HandshakeTelemetryHelpers.assertHistogramMap(values, histEntries); + } +} + +add_task(async function testH3WithNoEch() { + Services.prefs.setBoolPref("security.tls.ech.grease_http3", false); + Services.prefs.setIntPref("security.tls.ech.grease_probability", 0); + await H3ECHTest( + h3EchConfig, + "NONE", + new Map([ + ["0", 1], + ["1", 0], + ]), + false + ); +}); + +add_task(async function testH3WithECH() { + await H3ECHTest( + h3EchConfig, + "REAL", + new Map([ + ["0", 1], + ["1", 0], + ]), + true + ); +}); + +add_task(async function testH3WithGreaseEch() { + Services.prefs.setBoolPref("security.tls.ech.grease_http3", true); + Services.prefs.setIntPref("security.tls.ech.grease_probability", 100); + await H3ECHTest( + h3EchConfig, + "GREASE", + new Map([ + ["0", 1], + ["1", 0], + ]), + false + ); +}); + +add_task(async function testH3WithECHRetry() { + Services.dns.clearCache(true); + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + function base64ToArray(base64) { + var binary_string = atob(base64); + var len = binary_string.length; + var bytes = new Uint8Array(len); + for (var i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes; + } + + let decodedConfig = base64ToArray(h3EchConfig); + decodedConfig[6] ^= 0x94; + let encoded = btoa(String.fromCharCode.apply(null, decodedConfig)); + await H3ECHTest( + encoded, + "REAL", + new Map([ + ["0", 1], + ["1", 1], + ]), + true + ); +}); diff --git a/netwerk/test/unit/test_httpssvc_retry_without_ech.js b/netwerk/test/unit/test_httpssvc_retry_without_ech.js new file mode 100644 index 0000000000..8502ef492a --- /dev/null +++ b/netwerk/test/unit/test_httpssvc_retry_without_ech.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"; + +let trrServer; + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +add_setup(async function setup() { + trr_test_setup(); + + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + + // An arbitrary, non-ECH server. + await asyncStartTLSTestServer( + "DelegatedCredentialsServer", + "../../../security/manager/ssl/tests/unit/test_delegated_credentials" + ); + + let nssComponent = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent); + await nssComponent.asyncClearSSLExternalAndInternalSessionCache(); +}); + +registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + Services.prefs.clearUserPref("network.dns.echconfig.enabled"); + Services.prefs.clearUserPref( + "network.dns.echconfig.fallback_to_origin_when_all_failed" + ); + if (trrServer) { + await trrServer.stop(); + } +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + resolve([req, buffer]); + } + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +add_task(async function testRetryWithoutECH() { + const ECH_CONFIG_FIXED = + "AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAEAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA"; + trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref( + "network.dns.echconfig.fallback_to_origin_when_all_failed", + true + ); + + // Only the last record is valid to use. + await trrServer.registerDoHAnswers( + "delegated-disabled.example.com", + "HTTPS", + { + answers: [ + { + name: "delegated-disabled.example.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "delegated-disabled.example.com", + values: [ + { + key: "echconfig", + value: ECH_CONFIG_FIXED, + needBase64Decode: true, + }, + ], + }, + }, + ], + } + ); + + await trrServer.registerDoHAnswers("delegated-disabled.example.com", "A", { + answers: [ + { + name: "delegated-disabled.example.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await new TRRDNSListener("delegated-disabled.example.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let chan = makeChan(`https://delegated-disabled.example.com:8443`); + await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + let securityInfo = chan.securityInfo; + + Assert.ok( + !securityInfo.isAcceptedEch, + "This host should not have accepted ECH" + ); + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_httpsuspend.js b/netwerk/test/unit/test_httpsuspend.js new file mode 100644 index 0000000000..59ad64c143 --- /dev/null +++ b/netwerk/test/unit/test_httpsuspend.js @@ -0,0 +1,84 @@ +// This file ensures that suspending a channel directly after opening it +// suspends future notifications correctly. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +const MIN_TIME_DIFFERENCE = 3000; +const RESUME_DELAY = 5000; + +var listener = { + _lastEvent: 0, + _gotData: false, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + this._lastEvent = Date.now(); + request.QueryInterface(Ci.nsIRequest); + + // Insert a delay between this and the next callback to ensure message buffering + // works correctly + request.suspend(); + request.suspend(); + do_timeout(RESUME_DELAY, function () { + request.resume(); + }); + do_timeout(RESUME_DELAY + 1000, function () { + request.resume(); + }); + }, + + onDataAvailable(request, stream, offset, count) { + Assert.ok(Date.now() - this._lastEvent >= MIN_TIME_DIFFERENCE); + read_stream(stream, count); + + // Ensure that suspending and resuming inside a callback works correctly + request.suspend(); + request.suspend(); + request.resume(); + request.resume(); + + this._gotData = true; + }, + + onStopRequest(request, status) { + Assert.ok(this._gotData); + httpserv.stop(do_test_finished); + }, +}; + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var httpserv = null; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/woo", data); + httpserv.start(-1); + + var chan = makeChan(URL + "/woo"); + chan.QueryInterface(Ci.nsIRequest); + chan.asyncOpen(listener); + + do_test_pending(); +} + +function data(metadata, response) { + let httpbody = "0123456789"; + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(httpbody, httpbody.length); +} diff --git a/netwerk/test/unit/test_idn_blacklist.js b/netwerk/test/unit/test_idn_blacklist.js new file mode 100644 index 0000000000..565407ac3b --- /dev/null +++ b/netwerk/test/unit/test_idn_blacklist.js @@ -0,0 +1,168 @@ +// Test that URLs containing characters in the IDN blacklist are +// always displayed as punycode + +"use strict"; + +const testcases = [ + // Original Punycode or + // normalized form + // + ["\u00BC", "xn--14-c6t"], + ["\u00BD", "xn--12-c6t"], + ["\u00BE", "xn--34-c6t"], + ["\u01C3", "xn--ija"], + ["\u02D0", "xn--6qa"], + ["\u0337", "xn--4ta"], + ["\u0338", "xn--5ta"], + ["\u0589", "xn--3bb"], + ["\u05C3", "xn--rdb"], + ["\u05F4", "xn--5eb"], + ["\u0609", "xn--rfb"], + ["\u060A", "xn--sfb"], + ["\u066A", "xn--jib"], + ["\u06D4", "xn--klb"], + ["\u0701", "xn--umb"], + ["\u0702", "xn--vmb"], + ["\u0703", "xn--wmb"], + ["\u0704", "xn--xmb"], + ["\u115F", "xn--osd"], + ["\u1160", "xn--psd"], + ["\u1735", "xn--d0e"], + ["\u2027", "xn--svg"], + ["\u2028", "xn--tvg"], + ["\u2029", "xn--uvg"], + ["\u2039", "xn--bwg"], + ["\u203A", "xn--cwg"], + ["\u2041", "xn--jwg"], + ["\u2044", "xn--mwg"], + ["\u2052", "xn--0wg"], + ["\u2153", "xn--13-c6t"], + ["\u2154", "xn--23-c6t"], + ["\u2155", "xn--15-c6t"], + ["\u2156", "xn--25-c6t"], + ["\u2157", "xn--35-c6t"], + ["\u2158", "xn--45-c6t"], + ["\u2159", "xn--16-c6t"], + ["\u215A", "xn--56-c6t"], + ["\u215B", "xn--18-c6t"], + ["\u215C", "xn--38-c6t"], + ["\u215D", "xn--58-c6t"], + ["\u215E", "xn--78-c6t"], + ["\u215F", "xn--1-zjn"], + ["\u2215", "xn--w9g"], + ["\u2236", "xn--ubh"], + ["\u23AE", "xn--lmh"], + ["\u2571", "xn--hzh"], + ["\u29F6", "xn--jxi"], + ["\u29F8", "xn--lxi"], + ["\u2AFB", "xn--z4i"], + ["\u2AFD", "xn--14i"], + ["\u2FF0", "xn--85j"], + ["\u2FF1", "xn--95j"], + ["\u2FF2", "xn--b6j"], + ["\u2FF3", "xn--c6j"], + ["\u2FF4", "xn--d6j"], + ["\u2FF5", "xn--e6j"], + ["\u2FF6", "xn--f6j"], + ["\u2FF7", "xn--g6j"], + ["\u2FF8", "xn--h6j"], + ["\u2FF9", "xn--i6j"], + ["\u2FFA", "xn--j6j"], + ["\u2FFB", "xn--k6j"], + ["\u3014", "xn--96j"], + ["\u3015", "xn--b7j"], + ["\u3033", "xn--57j"], + ["\u3164", "xn--psd"], + ["\u321D", "xn--()-357j35d"], + ["\u321E", "xn--()-357jf36c"], + ["\u33AE", "xn--rads-id9a"], + ["\u33AF", "xn--rads2-4d6b"], + ["\u33C6", "xn--ckg-tc2a"], + ["\u33DF", "xn--am-6bv"], + ["\uA789", "xn--058a"], + ["\uFE3F", "xn--x6j"], + ["\uFE5D", "xn--96j"], + ["\uFE5E", "xn--b7j"], + ["\uFFA0", "xn--psd"], + ["\uFFF9", "xn--vn7c"], + ["\uFFFA", "xn--wn7c"], + ["\uFFFB", "xn--xn7c"], + ["\uFFFC", "xn--yn7c"], + ["\uFFFD", "xn--zn7c"], + + // Characters from the IDN blacklist that normalize to ASCII + // If we start using STD3ASCIIRules these will be blocked (bug 316444) + ["\u00A0", " "], + ["\u2000", " "], + ["\u2001", " "], + ["\u2002", " "], + ["\u2003", " "], + ["\u2004", " "], + ["\u2005", " "], + ["\u2006", " "], + ["\u2007", " "], + ["\u2008", " "], + ["\u2009", " "], + ["\u200A", " "], + ["\u2024", "."], + ["\u202F", " "], + ["\u205F", " "], + ["\u3000", " "], + ["\u3002", "."], + ["\uFE14", ";"], + ["\uFE15", "!"], + ["\uFF0E", "."], + ["\uFF0F", "/"], + ["\uFF61", "."], + + // Characters from the IDN blacklist that are stripped by Nameprep + ["\u200B", ""], + ["\uFEFF", ""], +]; + +function run_test() { + var pbi = Services.prefs; + var oldProfile = pbi.getCharPref( + "network.IDN.restriction_profile", + "moderate" + ); + var idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + + pbi.setCharPref("network.IDN.restriction_profile", "moderate"); + + for (var j = 0; j < testcases.length; ++j) { + var test = testcases[j]; + var URL = test[0] + ".com"; + var punycodeURL = test[1] + ".com"; + var isASCII = {}; + + var result; + try { + result = idnService.convertToDisplayIDN(URL, isASCII); + } catch (e) { + result = ".com"; + } + // If the punycode URL is equivalent to \ufffd.com (i.e. the + // blacklisted character has been replaced by a unicode + // REPLACEMENT CHARACTER, skip the test + if (result != "xn--zn7c.com") { + if (punycodeURL.substr(0, 4) == "xn--") { + // test convertToDisplayIDN with a Unicode URL and with a + // Punycode URL if we have one + equal(escape(result), escape(punycodeURL)); + + result = idnService.convertToDisplayIDN(punycodeURL, isASCII); + equal(escape(result), escape(punycodeURL)); + } else { + // The "punycode" URL isn't punycode. This happens in testcases + // where the Unicode URL has become normalized to an ASCII URL, + // so, even though expectedUnicode is true, the expected result + // is equal to punycodeURL + equal(escape(result), escape(punycodeURL)); + } + } + } + pbi.setCharPref("network.IDN.restriction_profile", oldProfile); +} diff --git a/netwerk/test/unit/test_idn_spoof.js b/netwerk/test/unit/test_idn_spoof.js new file mode 100644 index 0000000000..8512d3272e --- /dev/null +++ b/netwerk/test/unit/test_idn_spoof.js @@ -0,0 +1,1052 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// https://source.chromium.org/chromium/chromium/src/+/main:LICENSE + +// Tests nsIIDNService +// Imported from https://source.chromium.org/chromium/chromium/src/+/main:components/url_formatter/spoof_checks/idn_spoof_checker_unittest.cc;drc=e544837967287f956ba69af3b228b202e8e7cf1a + +"use strict"; + +const idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService +); + +const kSafe = 1; +const kUnsafe = 2; +const kInvalid = 3; + +// prettier-ignore +let testCases = [ + // No IDN + ["www.google.com", "www.google.com", kSafe], + ["www.google.com.", "www.google.com.", kSafe], + [".", ".", kSafe], + ["", "", kSafe], + // Invalid IDN + ["xn--example-.com", "xn--example-.com", kInvalid], + // IDN + // Hanzi (Traditional Chinese) + ["xn--1lq90ic7f1rc.cn", "\u5317\u4eac\u5927\u5b78.cn", kSafe], + // Hanzi ('video' in Simplified Chinese) + ["xn--cy2a840a.com", "\u89c6\u9891.com", kSafe], + // Hanzi + '123' + ["www.xn--123-p18d.com", "www.\u4e00123.com", kSafe], + // Hanzi + Latin : U+56FD is simplified + ["www.xn--hello-9n1hm04c.com", "www.hello\u4e2d\u56fd.com", kSafe], + // Kanji + Kana (Japanese) + ["xn--l8jvb1ey91xtjb.jp", "\u671d\u65e5\u3042\u3055\u3072.jp", kSafe], + // Katakana including U+30FC + ["xn--tckm4i2e.jp", "\u30b3\u30de\u30fc\u30b9.jp", kSafe], + ["xn--3ck7a7g.jp", "\u30ce\u30f3\u30bd.jp", kSafe], + // Katakana + Latin (Japanese) + ["xn--e-efusa1mzf.jp", "e\u30b3\u30de\u30fc\u30b9.jp", kSafe], + ["xn--3bkxe.jp", "\u30c8\u309a.jp", kSafe], + // Hangul (Korean) + ["www.xn--or3b17p6jjc.kr", "www.\uc804\uc790\uc815\ubd80.kr", kSafe], + // b<u-umlaut>cher (German) + ["xn--bcher-kva.de", "b\u00fccher.de", kSafe], + // a with diaeresis + ["www.xn--frgbolaget-q5a.se", "www.f\u00e4rgbolaget.se", kSafe], + // c-cedilla (French) + ["www.xn--alliancefranaise-npb.fr", "www.alliancefran\u00e7aise.fr", kSafe], + // caf'e with acute accent (French) + ["xn--caf-dma.fr", "caf\u00e9.fr", kSafe], + // c-cedillla and a with tilde (Portuguese) + ["xn--poema-9qae5a.com.br", "p\u00e3oema\u00e7\u00e3.com.br", kSafe], + // s with caron + ["xn--achy-f6a.com", "\u0161achy.com", kSafe], + ["xn--kxae4bafwg.gr", "\u03bf\u03c5\u03c4\u03bf\u03c0\u03af\u03b1.gr", kSafe], + // Eutopia + 123 (Greek) + ["xn---123-pldm0haj2bk.gr", "\u03bf\u03c5\u03c4\u03bf\u03c0\u03af\u03b1-123.gr", kSafe], + // Cyrillic (Russian) + ["xn--n1aeec9b.r", "\u0442\u043e\u0440\u0442\u044b.r", kSafe], + // Cyrillic + 123 (Russian) + ["xn---123-45dmmc5f.r", "\u0442\u043e\u0440\u0442\u044b-123.r", kSafe], + // 'president' in Russian. Is a wholescript confusable, but allowed. + ["xn--d1abbgf6aiiy.xn--p1ai", "\u043f\u0440\u0435\u0437\u0438\u0434\u0435\u043d\u0442.\u0440\u0444", kSafe], + // Arabic + ["xn--mgba1fmg.eg", "\u0627\u0641\u0644\u0627\u0645.eg", kSafe], + // Hebrew + ["xn--4dbib.he", "\u05d5\u05d0\u05d4.he", kSafe], + // Hebrew + Common + ["xn---123-ptf2c5c6bt.il", "\u05e2\u05d1\u05e8\u05d9\u05ea-123.il", kSafe], + // Thai + ["xn--12c2cc4ag3b4ccu.th", "\u0e2a\u0e32\u0e22\u0e01\u0e32\u0e23\u0e1a\u0e34\u0e19.th", kSafe], + // Thai + Common + ["xn---123-9goxcp8c9db2r.th", "\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22-123.th", kSafe], + // Devangari (Hindi) + ["www.xn--l1b6a9e1b7c.in", "www.\u0905\u0915\u094b\u0932\u093e.in", kSafe], + // Devanagari + Common + ["xn---123-kbjl2j0bl2k.in", "\u0939\u093f\u0928\u094d\u0926\u0940-123.in", kSafe], + + // Block mixed numeric + numeric lookalike (12.com, using U+0577). + ["xn--1-xcc.com", "1\u0577.com", kUnsafe, "DISABLED"], + + // Block mixed numeric lookalike + numeric (੨0.com, uses U+0A68). + ["xn--0-6ee.com", "\u0a680.com", kUnsafe], + // Block fully numeric lookalikes (৪੨.com using U+09EA and U+0A68). + ["xn--47b6w.com", "\u09ea\u0a68.com", kUnsafe], + // Block single script digit lookalikes (using three U+0A68 characters). + ["xn--qccaa.com", "\u0a68\u0a68\u0a68.com", kUnsafe, "DISABLED"], + + // URL test with mostly numbers and one confusable character + // Georgian 'd' 4000.com + ["xn--4000-pfr.com", "\u10eb4000.com", kUnsafe, "DISABLED"], + + // What used to be 5 Aspirational scripts in the earlier versions of UAX 31. + // UAX 31 does not define aspirational scripts any more. + // See http://www.unicode.org/reports/tr31/#Aspirational_Use_Scripts . + // Unified Canadian Syllabary + ["xn--dfe0tte.ca", "\u1456\u14c2\u14ef.ca", kUnsafe], + // Tifinagh + ["xn--4ljxa2bb4a6bxb.ma", "\u2d5c\u2d49\u2d3c\u2d49\u2d4f\u2d30\u2d56.ma", kUnsafe], + // Tifinagh with a disallowed character(U+2D6F) + ["xn--hmjzaby5d5f.ma", "\u2d5c\u2d49\u2d3c\u2d6f\u2d49\u2d4f.ma", kInvalid], + + // Yi + ["xn--4o7a6e1x64c.cn", "\ua188\ua320\ua071\ua0b7.cn", kUnsafe], + // Mongolian - 'ordu' (place, camp) + ["xn--56ec8bp.cn", "\u1823\u1837\u1833\u1824.cn", kUnsafe], + // Mongolian with a disallowed character + ["xn--95e5de3ds.cn", "\u1823\u1837\u1804\u1833\u1824.cn", kUnsafe], + // Miao/Pollad + ["xn--2u0fpf0a.cn", "\U00016f04\U00016f62\U00016f59.cn", kUnsafe], + + // Script mixing tests + // The following script combinations are allowed. + // HIGHLY_RESTRICTIVE with Latin limited to ASCII-Latin. + // ASCII-Latin + Japn (Kana + Han) + // ASCII-Latin + Kore (Hangul + Han) + // ASCII-Latin + Han + Bopomofo + // "payp<alpha>l.com" + ["xn--paypl-g9d.com", "payp\u03b1l.com", kUnsafe], + // google.gr with Greek omicron and epsilon + ["xn--ggl-6xc1ca.gr", "g\u03bf\u03bfgl\u03b5.gr", kUnsafe], + // google.ru with Cyrillic o + ["xn--ggl-tdd6ba.r", "g\u043e\u043egl\u0435.r", kUnsafe], + // h<e with acute>llo<China in Han>.cn + ["xn--hllo-bpa7979ih5m.cn", "h\u00e9llo\u4e2d\u56fd.cn", kUnsafe, "DISABLED"], + // <Greek rho><Cyrillic a><Cyrillic u>.ru + ["xn--2xa6t2b.r", "\u03c1\u0430\u0443.r", kUnsafe], + // Georgian + Latin + ["xn--abcef-vuu.test", "abc\u10ebef.test", kUnsafe], + // Hangul + Latin + ["xn--han-eb9ll88m.kr", "\ud55c\uae00han.kr", kSafe], + // Hangul + Latin + Han with IDN ccTLD + ["xn--han-or0kq92gkm3c.xn--3e0b707e", "\ud55c\uae00han\u97d3.\ud55c\uad6d", kSafe], + // non-ASCII Latin + Hangul + ["xn--caf-dma9024xvpg.kr", "caf\u00e9\uce74\ud398.kr", kUnsafe, "DISABLED"], + // Hangul + Hiragana + ["xn--y9j3b9855e.kr", "\ud55c\u3072\u3089.kr", kUnsafe], + // <Hiragana>.<Hangul> is allowed because script mixing check is per label. + ["xn--y9j3b.xn--3e0b707e", "\u3072\u3089.\ud55c\uad6d", kSafe], + // Traditional Han + Latin + ["xn--hanzi-u57ii69i.tw", "\u6f22\u5b57hanzi.tw", kSafe], + // Simplified Han + Latin + ["xn--hanzi-u57i952h.cn", "\u6c49\u5b57hanzi.cn", kSafe], + // Simplified Han + Traditonal Han + ["xn--hanzi-if9kt8n.cn", "\u6c49\u6f22hanzi.cn", kSafe], + // Han + Hiragana + Katakana + Latin + ["xn--kanji-ii4dpizfq59yuykqr4b.jp", "\u632f\u308a\u4eee\u540d\u30ab\u30bfkanji.jp", kSafe], + // Han + Bopomofo + ["xn--5ekcde0577e87tc.tw", "\u6ce8\u97f3\u3105\u3106\u3107\u3108.tw", kSafe], + // Han + Latin + Bopomofo + ["xn--bopo-ty4cghi8509kk7xd.tw", "\u6ce8\u97f3bopo\u3105\u3106\u3107\u3108.tw", kSafe], + // Latin + Bopomofo + ["xn--bopomofo-hj5gkalm.tw", "bopomofo\u3105\u3106\u3107\u3108.tw", kSafe], + // Bopomofo + Katakana + ["xn--lcka3d1bztghi.tw", "\u3105\u3106\u3107\u3108\u30ab\u30bf\u30ab\u30ca.tw", kUnsafe], + // Bopomofo + Hangul + ["xn--5ekcde4543qbec.tw", "\u3105\u3106\u3107\u3108\uc8fc\uc74c.tw", kUnsafe], + // Devanagari + Latin + ["xn--ab-3ofh8fqbj6h.in", "ab\u0939\u093f\u0928\u094d\u0926\u0940.in", kUnsafe], + // Thai + Latin + ["xn--ab-jsi9al4bxdb6n.th", "ab\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22.th", kUnsafe], + // Armenian + Latin + ["xn--bs-red.com", "b\u057ds.com", kUnsafe], + // Tibetan + Latin + ["xn--foo-vkm.com", "foo\u0f37.com", kUnsafe], + // Oriya + Latin + ["xn--fo-h3g.com", "fo\u0b66.com", kUnsafe], + // Gujarati + Latin + ["xn--fo-isg.com", "fo\u0ae6.com", kUnsafe], + // <vitamin in Katakana>b1.com + ["xn--b1-xi4a7cvc9f.com", "\u30d3\u30bf\u30df\u30f3b1.com", kSafe], + // Devanagari + Han + ["xn--t2bes3ds6749n.com", "\u0930\u094b\u0932\u0947\u76e7\u0938.com", kUnsafe], + // Devanagari + Bengali + ["xn--11b0x.in", "\u0915\u0995.in", kUnsafe], + // Canadian Syllabary + Latin + ["xn--ab-lym.com", "ab\u14bf.com", kUnsafe], + ["xn--ab1-p6q.com", "ab1\u14bf.com", kUnsafe], + ["xn--1ab-m6qd.com", "\u14bf1ab\u14bf.com", kUnsafe], + ["xn--ab-jymc.com", "\u14bfab\u14bf.com", kUnsafe], + // Tifinagh + Latin + ["xn--liy-bq1b.com", "li\u2d4fy.com", kUnsafe], + ["xn--rol-cq1b.com", "rol\u2d4f.com", kUnsafe], + ["xn--ily-8p1b.com", "\u2d4fily.com", kUnsafe], + ["xn--1ly-8p1b.com", "\u2d4f1ly.com", kUnsafe], + + // Invisibility check + // Thai tone mark malek(U+0E48) repeated + ["xn--03c0b3ca.th", "\u0e23\u0e35\u0e48\u0e48.th", kUnsafe], + // Accute accent repeated + ["xn--a-xbba.com", "a\u0301\u0301.com", kInvalid], + // 'a' with acuted accent + another acute accent + ["xn--1ca20i.com", "\u00e1\u0301.com", kUnsafe, "DISABLED"], + // Combining mark at the beginning + ["xn--abc-fdc.jp", "\u0300abc.jp", kInvalid], + + // The following three are detected by |dangerous_pattern| regex, but + // can be regarded as an extension of blocking repeated diacritic marks. + // i followed by U+0307 (combining dot above) + ["xn--pixel-8fd.com", "pi\u0307xel.com", kUnsafe], + // U+0131 (dotless i) followed by U+0307 + ["xn--pxel-lza43z.com", "p\u0131\u0307xel.com", kUnsafe], + // j followed by U+0307 (combining dot above) + ["xn--jack-qwc.com", "j\u0307ack.com", kUnsafe], + // l followed by U+0307 + ["xn--lace-qwc.com", "l\u0307ace.com", kUnsafe], + + // Do not allow a combining mark after dotless i/j. + ["xn--pxel-lza29y.com", "p\u0131\u0300xel.com", kUnsafe], + ["xn--ack-gpb42h.com", "\u0237\u0301ack.com", kUnsafe], + + // Mixed script confusable + // google with Armenian Small Letter Oh(U+0585) + ["xn--gogle-lkg.com", "g\u0585ogle.com", kUnsafe], + ["xn--range-kkg.com", "\u0585range.com", kUnsafe], + ["xn--cucko-pkg.com", "cucko\u0585.com", kUnsafe], + // Latin 'o' in Armenian. + ["xn--o-ybcg0cu0cq.com", "o\u0580\u0574\u0578\u0582\u0566\u0568.com", kUnsafe], + // Hiragana HE(U+3078) mixed with Katakana + ["xn--49jxi3as0d0fpc.com", "\u30e2\u30d2\u30fc\u30c8\u3078\u30d6\u30f3.com", kUnsafe, "DISABLED"], + + // U+30FC should be preceded by a Hiragana/Katakana. + // Katakana + U+30FC + Han + ["xn--lck0ip02qw5ya.jp", "\u30ab\u30fc\u91ce\u7403.jp", kSafe], + // Hiragana + U+30FC + Han + ["xn--u8j5tr47nw5ya.jp", "\u304b\u30fc\u91ce\u7403.jp", kSafe], + // U+30FC + Han + ["xn--weka801xo02a.com", "\u30fc\u52d5\u753b\u30fc.com", kUnsafe], + // Han + U+30FC + Han + ["xn--wekz60nb2ay85atj0b.jp", "\u65e5\u672c\u30fc\u91ce\u7403.jp", kUnsafe], + // U+30FC at the beginning + ["xn--wek060nb2a.jp", "\u30fc\u65e5\u672c.jp", kUnsafe], + // Latin + U+30FC + Latin + ["xn--abcdef-r64e.jp", "abc\u30fcdef.jp", kUnsafe], + + // U+30FB (・) is not allowed next to Latin, but allowed otherwise. + // U+30FB + Han + ["xn--vekt920a.jp", "\u30fb\u91ce.jp", kSafe], + // Han + U+30FB + Han + ["xn--vek160nb2ay85atj0b.jp", "\u65e5\u672c\u30fb\u91ce\u7403.jp", kSafe], + // Latin + U+30FB + Latin + ["xn--abcdef-k64e.jp", "abc\u30fbdef.jp", kUnsafe, "DISABLED"], + // U+30FB + Latin + ["xn--abc-os4b.jp", "\u30fbabc.jp", kUnsafe, "DISABLED"], + + // U+30FD (ヽ) is allowed only after Katakana. + // Katakana + U+30FD + ["xn--lck2i.jp", "\u30ab\u30fd.jp", kSafe], + // Hiragana + U+30FD + ["xn--u8j7t.jp", "\u304b\u30fd.jp", kUnsafe, "DISABLED"], + // Han + U+30FD + ["xn--xek368f.jp", "\u4e00\u30fd.jp", kUnsafe, "DISABLED"], + ["xn--a-mju.jp", "a\u30fd.jp", kUnsafe, "DISABLED"], + ["xn--a1-bo4a.jp", "a1\u30fd.jp", kUnsafe, "DISABLED"], + + // U+30FE (ヾ) is allowed only after Katakana. + // Katakana + U+30FE + ["xn--lck4i.jp", "\u30ab\u30fe.jp", kSafe], + // Hiragana + U+30FE + ["xn--u8j9t.jp", "\u304b\u30fe.jp", kUnsafe, "DISABLED"], + // Han + U+30FE + ["xn--yek168f.jp", "\u4e00\u30fe.jp", kUnsafe, "DISABLED"], + ["xn--a-oju.jp", "a\u30fe.jp", kUnsafe, "DISABLED"], + ["xn--a1-eo4a.jp", "a1\u30fe.jp", kUnsafe, "DISABLED"], + + // Cyrillic labels made of Latin-look-alike Cyrillic letters. + // 1) ѕсоре.com with ѕсоре in Cyrillic. + ["xn--e1argc3h.com", "\u0455\u0441\u043e\u0440\u0435.com", kUnsafe, "DISABLED"], + // 2) ѕсоре123.com with ѕсоре in Cyrillic. + ["xn--123-qdd8bmf3n.com", "\u0455\u0441\u043e\u0440\u0435123.com", kUnsafe, "DISABLED"], + // 3) ѕсоре-рау.com with ѕсоре and рау in Cyrillic. + ["xn----8sbn9akccw8m.com", "\u0455\u0441\u043e\u0440\u0435-\u0440\u0430\u0443.com", kUnsafe, "DISABLED"], + // 4) ѕсоре1рау.com with scope and pay in Cyrillic and a non-letter between + // them. + ["xn--1-8sbn9akccw8m.com", "\u0455\u0441\u043e\u0440\u0435\u0031\u0440\u0430\u0443.com", kUnsafe, "DISABLED"], + + // The same as above three, but in IDN TLD (рф). + // 1) ѕсоре.рф with ѕсоре in Cyrillic. + ["xn--e1argc3h.xn--p1ai", "\u0455\u0441\u043e\u0440\u0435.\u0440\u0444", kSafe], + // 2) ѕсоре123.рф with ѕсоре in Cyrillic. + ["xn--123-qdd8bmf3n.xn--p1ai", "\u0455\u0441\u043e\u0440\u0435123.\u0440\u0444", kSafe], + // 3) ѕсоре-рау.рф with ѕсоре and рау in Cyrillic. + ["xn----8sbn9akccw8m.xn--p1ai", "\u0455\u0441\u043e\u0440\u0435-\u0440\u0430\u0443.\u0440\u0444", kSafe], + // 4) ѕсоре1рау.com with scope and pay in Cyrillic and a non-letter between + // them. + ["xn--1-8sbn9akccw8m.xn--p1ai", "\u0455\u0441\u043e\u0440\u0435\u0031\u0440\u0430\u0443.\u0440\u0444", kSafe], + + // Same as above three, but in .ru TLD. + // 1) ѕсоре.ru with ѕсоре in Cyrillic. + ["xn--e1argc3h.r", "\u0455\u0441\u043e\u0440\u0435.r", kSafe], + // 2) ѕсоре123.ru with ѕсоре in Cyrillic. + ["xn--123-qdd8bmf3n.r", "\u0455\u0441\u043e\u0440\u0435123.r", kSafe], + // 3) ѕсоре-рау.ru with ѕсоре and рау in Cyrillic. + ["xn----8sbn9akccw8m.r", "\u0455\u0441\u043e\u0440\u0435-\u0440\u0430\u0443.r", kSafe], + // 4) ѕсоре1рау.com with scope and pay in Cyrillic and a non-letter between + // them. + ["xn--1-8sbn9akccw8m.r", "\u0455\u0441\u043e\u0440\u0435\u0031\u0440\u0430\u0443.r", kSafe], + + // ѕсоре-рау.한국 with ѕсоре and рау in Cyrillic. The label will remain + // punycode while the TLD will be decoded. + ["xn----8sbn9akccw8m.xn--3e0b707e", "xn----8sbn9akccw8m.\ud55c\uad6d", kSafe, "DISABLED"], + + // музей (museum in Russian) has characters without a Latin-look-alike. + ["xn--e1adhj9a.com", "\u043c\u0443\u0437\u0435\u0439.com", kSafe], + + // ѕсоԗе.com is Cyrillic with Latin lookalikes. + ["xn--e1ari3f61c.com", "\u0455\u0441\u043e\u0517\u0435.com", kUnsafe, "DISABLED"], + + // ыоԍ.com is Cyrillic with Latin lookalikes. + ["xn--n1az74c.com", "\u044b\u043e\u050d.com", kUnsafe], + + // сю.com is Cyrillic with Latin lookalikes. + ["xn--q1a0a.com", "\u0441\u044e.com", kUnsafe, "DISABLED"], + + // Regression test for lowercase letters in whole script confusable + // lookalike character lists. + ["xn--80a8a6a.com", "\u0430\u044c\u0441.com", kUnsafe, "DISABLED"], + + // googlе.한국 where е is Cyrillic. This tests the generic case when one + // label is not allowed but other labels in the domain name are still + // decoded. Here, googlе is left in punycode but the TLD is decoded. + ["xn--googl-3we.xn--3e0b707e", "xn--googl-3we.\ud55c\uad6d", kSafe], + + // Combining Diacritic marks after a script other than Latin-Greek-Cyrillic + ["xn--rsa2568fvxya.com", "\ud55c\u0307\uae00.com", kUnsafe, "DISABLED"], // 한́글.com + ["xn--rsa0336bjom.com", "\u6f22\u0307\u5b57.com", kUnsafe, "DISABLED"], // 漢̇字.com + // नागरी́.com + ["xn--lsa922apb7a6do.com", "\u0928\u093e\u0917\u0930\u0940\u0301.com", kUnsafe, "DISABLED"], + + // Similarity checks against the list of top domains. "digklmo68.com" and + // 'digklmo68.co.uk" are listed for unittest in the top domain list. + // đigklmo68.com: + ["xn--igklmo68-kcb.com", "\u0111igklmo68.com", kUnsafe, "DISABLED"], + // www.đigklmo68.com: + ["www.xn--igklmo68-kcb.com", "www.\u0111igklmo68.com", kUnsafe, "DISABLED"], + // foo.bar.đigklmo68.com: + ["foo.bar.xn--igklmo68-kcb.com", "foo.bar.\u0111igklmo68.com", kUnsafe, "DISABLED"], + // đigklmo68.co.uk: + ["xn--igklmo68-kcb.co.uk", "\u0111igklmo68.co.uk", kUnsafe, "DISABLED"], + // mail.đigklmo68.co.uk: + ["mail.xn--igklmo68-kcb.co.uk", "mail.\u0111igklmo68.co.uk", kUnsafe, "DISABLED"], + // di̇gklmo68.com: + ["xn--digklmo68-6jf.com", "di\u0307gklmo68.com", kUnsafe], + // dig̱klmo68.com: + ["xn--digklmo68-7vf.com", "dig\u0331klmo68.com", kUnsafe, "DISABLED"], + // digĸlmo68.com: + ["xn--diglmo68-omb.com", "dig\u0138lmo68.com", kUnsafe], + // digkłmo68.com: + ["xn--digkmo68-9ob.com", "digk\u0142mo68.com", kUnsafe, "DISABLED"], + // digklṃo68.com: + ["xn--digklo68-l89c.com", "digkl\u1e43o68.com", kUnsafe, "DISABLED"], + // digklmø68.com: + ["xn--digklm68-b5a.com", "digklm\u00f868.com", kUnsafe, "DISABLED"], + // digklmoб8.com: + ["xn--digklmo8-h7g.com", "digklmo\u04318.com", kUnsafe], + // digklmo6৪.com: + ["xn--digklmo6-7yr.com", "digklmo6\u09ea.com", kUnsafe], + + // 'islkpx123.com' is in the test domain list. + // 'іѕӏкрх123' can look like 'islkpx123' in some fonts. + ["xn--123-bed4a4a6hh40i.com", "\u0456\u0455\u04cf\u043a\u0440\u0445123.com", kUnsafe, "DISABLED"], + + // 'o2.com', '28.com', '39.com', '43.com', '89.com', 'oo.com' and 'qq.com' + // are all explicitly added to the test domain list to aid testing of + // Latin-lookalikes that are numerics in other character sets and similar + // edge cases. + // + // Bengali: + ["xn--07be.com", "\u09e6\u09e8.com", kUnsafe, "DISABLED"], + ["xn--27be.com", "\u09e8\u09ea.com", kUnsafe, "DISABLED"], + ["xn--77ba.com", "\u09ed\u09ed.com", kUnsafe, "DISABLED"], + // Gurmukhi: + ["xn--qcce.com", "\u0a68\u0a6a.com", kUnsafe, "DISABLED"], + ["xn--occe.com", "\u0a66\u0a68.com", kUnsafe, "DISABLED"], + ["xn--rccd.com", "\u0a6b\u0a69.com", kUnsafe, "DISABLED"], + ["xn--pcca.com", "\u0a67\u0a67.com", kUnsafe, "DISABLED"], + // Telugu: + ["xn--drcb.com", "\u0c69\u0c68.com", kUnsafe, "DISABLED"], + // Devanagari: + ["xn--d4be.com", "\u0966\u0968.com", kUnsafe, "DISABLED"], + // Kannada: + ["xn--yucg.com", "\u0ce6\u0ce9.com", kUnsafe, "DISABLED"], + ["xn--yuco.com", "\u0ce6\u0ced.com", kUnsafe, "DISABLED"], + // Oriya: + ["xn--1jcf.com", "\u0b6b\u0b68.com", kUnsafe, "DISABLED"], + ["xn--zjca.com", "\u0b66\u0b66.com", kUnsafe, "DISABLED"], + // Gujarati: + ["xn--cgce.com", "\u0ae6\u0ae8.com", kUnsafe, "DISABLED"], + ["xn--fgci.com", "\u0ae9\u0aed.com", kUnsafe, "DISABLED"], + ["xn--dgca.com", "\u0ae7\u0ae7.com", kUnsafe, "DISABLED"], + + // wmhtb.com + ["xn--l1acpvx.com", "\u0448\u043c\u043d\u0442\u044c.com", kUnsafe, "DISABLED"], + // щмнть.com + ["xn--l1acpzs.com", "\u0449\u043c\u043d\u0442\u044c.com", kUnsafe, "DISABLED"], + // шмнтв.com + ["xn--b1atdu1a.com", "\u0448\u043c\u043d\u0442\u0432.com", kUnsafe, "DISABLED"], + // шмԋтв.com + ["xn--b1atsw09g.com", "\u0448\u043c\u050b\u0442\u0432.com", kUnsafe], + // шмԧтв.com + ["xn--b1atsw03i.com", "\u0448\u043c\u0527\u0442\u0432.com", kUnsafe, "DISABLED"], + // шмԋԏв.com + ["xn--b1at9a12dua.com", "\u0448\u043c\u050b\u050f\u0432.com", kUnsafe], + // ഠട345.com + ["xn--345-jtke.com", "\u0d20\u0d1f345.com", kUnsafe, "DISABLED"], + + // Test additional confusable LGC characters (most of them without + // decomposition into base + diacritc mark). The corresponding ASCII + // domain names are in the test top domain list. + // ϼκαωχ.com + ["xn--mxar4bh6w.com", "\u03fc\u03ba\u03b1\u03c9\u03c7.com", kUnsafe, "DISABLED"], + // þħĸŧƅ.com + ["xn--vda6f3b2kpf.com", "\u00fe\u0127\u0138\u0167\u0185.com", kUnsafe], + // þhktb.com + ["xn--hktb-9ra.com", "\u00fehktb.com", kUnsafe, "DISABLED"], + // pħktb.com + ["xn--pktb-5xa.com", "p\u0127ktb.com", kUnsafe, "DISABLED"], + // phĸtb.com + ["xn--phtb-m0a.com", "ph\u0138tb.com", kUnsafe], + // phkŧb.com + ["xn--phkb-d7a.com", "phk\u0167b.com", kUnsafe, "DISABLED"], + // phktƅ.com + ["xn--phkt-ocb.com", "phkt\u0185.com", kUnsafe], + // ҏнкть.com + ["xn--j1afq4bxw.com", "\u048f\u043d\u043a\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏћкть.com + ["xn--j1aq4a7cvo.com", "\u048f\u045b\u043a\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏңкть.com + ["xn--j1aq4azund.com", "\u048f\u04a3\u043a\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏҥкть.com + ["xn--j1aq4azuxd.com", "\u048f\u04a5\u043a\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏӈкть.com + ["xn--j1aq4azuyj.com", "\u048f\u04c8\u043a\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏԧкть.com + ["xn--j1aq4azu9z.com", "\u048f\u0527\u043a\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏԩкть.com + ["xn--j1aq4azuq0a.com", "\u048f\u0529\u043a\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏнқть.com + ["xn--m1ak4azu6b.com", "\u048f\u043d\u049b\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏнҝть.com + ["xn--m1ak4azunc.com", "\u048f\u043d\u049d\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏнҟть.com + ["xn--m1ak4azuxc.com", "\u048f\u043d\u049f\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏнҡть.com + ["xn--m1ak4azu7c.com", "\u048f\u043d\u04a1\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏнӄть.com + ["xn--m1ak4azu8i.com", "\u048f\u043d\u04c4\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏнԟть.com + ["xn--m1ak4azuzy.com", "\u048f\u043d\u051f\u0442\u044c.com", kUnsafe, "DISABLED"], + // ҏнԟҭь.com + ["xn--m1a4a4nnery.com", "\u048f\u043d\u051f\u04ad\u044c.com", kUnsafe, "DISABLED"], + // ҏнԟҭҍ.com + ["xn--m1a4ne5jry.com", "\u048f\u043d\u051f\u04ad\u048d.com", kUnsafe, "DISABLED"], + // ҏнԟҭв.com + ["xn--b1av9v8dry.com", "\u048f\u043d\u051f\u04ad\u0432.com", kUnsafe, "DISABLED"], + // ҏӊԟҭв.com + ["xn--b1a9p8c1e8r.com", "\u048f\u04ca\u051f\u04ad\u0432.com", kUnsafe, "DISABLED"], + // wmŋr.com + ["xn--wmr-jxa.com", "wm\u014br.com", kUnsafe, "DISABLED"], + // шмпґ.com + ["xn--l1agz80a.com", "\u0448\u043c\u043f\u0491.com", kUnsafe, "DISABLED"], + // щмпґ.com + ["xn--l1ag2a0y.com", "\u0449\u043c\u043f\u0491.com", kUnsafe, "DISABLED"], + // щӎпґ.com + ["xn--o1at1tsi.com", "\u0449\u04ce\u043f\u0491.com", kUnsafe, "DISABLED"], + // ґғ.com + ["xn--03ae.com", "\u0491\u0493.com", kUnsafe, "DISABLED"], + // ґӻ.com + ["xn--03a6s.com", "\u0491\u04fb.com", kUnsafe, "DISABLED"], + // ҫұҳҽ.com + ["xn--r4amg4b.com", "\u04ab\u04b1\u04b3\u04bd.com", kUnsafe, "DISABLED"], + // ҫұӽҽ.com + ["xn--r4am0b8r.com", "\u04ab\u04b1\u04fd\u04bd.com", kUnsafe, "DISABLED"], + // ҫұӿҽ.com + ["xn--r4am0b3s.com", "\u04ab\u04b1\u04ff\u04bd.com", kUnsafe, "DISABLED"], + // ҫұӿҿ.com + ["xn--r4am6b4p.com", "\u04ab\u04b1\u04ff\u04bf.com", kUnsafe, "DISABLED"], + // ҫұӿє.com + ["xn--91a7osa62a.com", "\u04ab\u04b1\u04ff\u0454.com", kUnsafe, "DISABLED"], + // ӏԃԍ.com + ["xn--s5a8h4a.com", "\u04cf\u0503\u050d.com", kUnsafe], + + // U+04CF(ӏ) is mapped to multiple characters, lowercase L(l) and + // lowercase I(i). Lowercase L is also regarded as similar to digit 1. + // The test domain list has {ig, ld, 1gd}.com for Cyrillic. + // ӏԍ.com + ["xn--s5a8j.com", "\u04cf\u050d.com", kUnsafe], + // ӏԃ.com + ["xn--s5a8h.com", "\u04cf\u0503.com", kUnsafe], + // ӏԍԃ.com + ["xn--s5a8h3a.com", "\u04cf\u050d\u0503.com", kUnsafe], + + // 1շ34567890.com + ["xn--134567890-gnk.com", "1\u057734567890.com", kUnsafe, "DISABLED"], + // ꓲ2345б7890.com + ["xn--23457890-e7g93622b.com", "\ua4f22345\u04317890.com", kUnsafe], + // 1ᒿ345б7890.com + ["xn--13457890-e7g0943b.com", "1\u14bf345\u04317890.com", kUnsafe], + // 12з4567890.com + ["xn--124567890-10h.com", "12\u04374567890.com", kUnsafe, "DISABLED"], + // 12ҙ4567890.com + ["xn--124567890-1ti.com", "12\u04994567890.com", kUnsafe, "DISABLED"], + // 12ӡ4567890.com + ["xn--124567890-mfj.com", "12\u04e14567890.com", kUnsafe, "DISABLED"], + // 12उ4567890.com + ["xn--124567890-m3r.com", "12\u09094567890.com", kUnsafe, "DISABLED"], + // 12ও4567890.com + ["xn--124567890-17s.com", "12\u09934567890.com", kUnsafe, "DISABLED"], + // 12ਤ4567890.com + ["xn--124567890-hfu.com", "12\u0a244567890.com", kUnsafe, "DISABLED"], + // 12ဒ4567890.com + ["xn--124567890-6s6a.com", "12\u10124567890.com", kUnsafe, "DISABLED"], + // 12ვ4567890.com + ["xn--124567890-we8a.com", "12\u10D54567890.com", kUnsafe, "DISABLED"], + // 12პ4567890.com + ["xn--124567890-hh8a.com", "12\u10DE4567890.com", kUnsafe, "DISABLED"], + // 123ㄐ567890.com + ["xn--123567890-dr5h.com", "123ㄐ567890.com", kUnsafe, "DISABLED"], + // 123Ꮞ567890.com + ["xn--123567890-dm4b.com", "123\u13ce567890.com", kUnsafe], + // 12345б7890.com + ["xn--123457890-fzh.com", "12345\u04317890.com", kUnsafe, "DISABLED"], + // 12345ճ7890.com + ["xn--123457890-fmk.com", "12345ճ7890.com", kUnsafe, "DISABLED"], + // 1234567ȣ90.com + ["xn--123456790-6od.com", "1234567\u022390.com", kUnsafe], + // 12345678୨0.com + ["xn--123456780-71w.com", "12345678\u0b680.com", kUnsafe], + // 123456789ଠ.com + ["xn--123456789-ohw.com", "123456789\u0b20.com", kUnsafe, "DISABLED"], + // 123456789ꓳ.com + ["xn--123456789-tx75a.com", "123456789\ua4f3.com", kUnsafe], + + // aeœ.com + ["xn--ae-fsa.com", "ae\u0153.com", kUnsafe, "DISABLED"], + // æce.com + ["xn--ce-0ia.com", "\u00e6ce.com", kUnsafe, "DISABLED"], + // æœ.com + ["xn--6ca2t.com", "\u00e6\u0153.com", kUnsafe, "DISABLED"], + // ӕԥ.com + ["xn--y5a4n.com", "\u04d5\u0525.com", kUnsafe, "DISABLED"], + + // ငၔဌ၂ဝ.com (entirely made of Myanmar characters) + ["xn--ridq5c9hnd.com", "\u1004\u1054\u100c\u1042\u101d.com", kUnsafe, "DISABLED"], + + // ฟรฟร.com (made of two Thai characters. similar to wsws.com in + // some fonts) + ["xn--w3calb.com", "\u0e1f\u0e23\u0e1f\u0e23.com", kUnsafe, "DISABLED"], + // พรบ.com + ["xn--r3chp.com", "\u0e1e\u0e23\u0e1a.com", kUnsafe, "DISABLED"], + // ฟรบ.com + ["xn--r3cjm.com", "\u0e1f\u0e23\u0e1a.com", kUnsafe, "DISABLED"], + + // Lao characters that look like w, s, o, and u. + // ພຣບ.com + ["xn--f7chp.com", "\u0e9e\u0ea3\u0e9a.com", kUnsafe, "DISABLED"], + // ຟຣບ.com + ["xn--f7cjm.com", "\u0e9f\u0ea3\u0e9a.com", kUnsafe, "DISABLED"], + // ຟຮບ.com + ["xn--f7cj9b.com", "\u0e9f\u0eae\u0e9a.com", kUnsafe, "DISABLED"], + // ຟຮ໐ບ.com + ["xn--f7cj9b5h.com", "\u0e9f\u0eae\u0ed0\u0e9a.com", kUnsafe, "DISABLED"], + + // Lao character that looks like n. + // ก11.com + ["xn--11-lqi.com", "\u0e0111.com", kUnsafe, "DISABLED"], + + // At one point the skeleton of 'w' was 'vv', ensure that + // that it's treated as 'w'. + ["xn--wder-qqa.com", "w\u00f3der.com", kUnsafe, "DISABLED"], + + // Mixed digits: the first two will also fail mixed script test + // Latin + ASCII digit + Deva digit + ["xn--asc1deva-j0q.co.in", "asc1deva\u0967.co.in", kUnsafe], + // Latin + Deva digit + Beng digit + ["xn--devabeng-f0qu3f.co.in", "deva\u0967beng\u09e7.co.in", kUnsafe], + // ASCII digit + Deva digit + ["xn--79-v5f.co.in", "7\u09ea9.co.in", kUnsafe], + // Deva digit + Beng digit + ["xn--e4b0x.co.in", "\u0967\u09e7.co.in", kUnsafe], + // U+4E00 (CJK Ideograph One) is not a digit, but it's not allowed next to + // non-Kana scripts including numbers. + ["xn--d12-s18d.cn", "d12\u4e00.cn", kUnsafe, "DISABLED"], + // One that's really long that will force a buffer realloc + ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", kSafe], + + // Not allowed; characters outside [:Identifier_Status=Allowed:] + // Limited Use Scripts: UTS 31 Table 7. + // Vai + ["xn--sn8a.com", "\ua50b.com", kUnsafe], + // 'CARD' look-alike in Cherokee + ["xn--58db0a9q.com", "\u13df\u13aa\u13a1\u13a0.com", kUnsafe], + // Scripts excluded from Identifiers: UTS 31 Table 4 + // Coptic + ["xn--5ya.com", "\u03e7.com", kUnsafe], + // Old Italic + ["xn--097cc.com", "\U00010300\U00010301.com", kUnsafe], + + // U+115F (Hangul Filler) + ["xn--osd3820f24c.kr", "\uac00\ub098\u115f.kr", kInvalid], + ["www.xn--google-ho0coa.com", "www.\u2039google\u203a.com", kUnsafe], + // Latin small capital w: hardᴡare.com + ["xn--hardare-l41c.com", "hard\u1d21are.com", kUnsafe], + // Minus Sign(U+2212) + ["xn--t9g238xc2a.jp", "\u65e5\u2212\u672c.jp", kUnsafe], + // Latin Small Letter Script G: ɡɡ.com + ["xn--0naa.com", "\u0261\u0261.com", kUnsafe], + // Hangul Jamo(U+11xx) + ["xn--0pdc3b.com", "\u1102\u1103\u1110.com", kUnsafe], + // degree sign: 36°c.com + ["xn--36c-tfa.com", "36\u00b0c.com", kUnsafe], + // Pound sign + ["xn--5free-fga.com", "5free\u00a3.com", kUnsafe], + // Hebrew points (U+05B0, U+05B6) + ["xn--7cbl2kc2a.com", "\u05e1\u05b6\u05e7\u05b0\u05e1.com", kUnsafe], + // Danda(U+0964) + ["xn--81bp1b6ch8s.com", "\u0924\u093f\u091c\u0964\u0930\u0940.com", kUnsafe], + // Small letter script G(U+0261) + ["xn--oogle-qmc.com", "\u0261oogle.com", kUnsafe], + // Small Katakana Extension(U+31F1) + ["xn--wlk.com", "\u31f1.com", kUnsafe], + // Heart symbol: ♥ + ["xn--ab-u0x.com", "ab\u2665.com", kUnsafe], + // Emoji + ["xn--vi8hiv.xyz", "\U0001f355\U0001f4a9.xyz", kUnsafe], + // Registered trade mark + ["xn--egistered-fna.com", "\u00aeegistered.com", kUnsafe], + // Latin Letter Retroflex Click + ["xn--registered-25c.com", "registered\u01c3.com", kUnsafe], + // ASCII '!' not allowed in IDN + ["xn--!-257eu42c.kr", "\uc548\ub155!.kr", kUnsafe], + // 'GOOGLE' in IPA extension: ɢᴏᴏɢʟᴇ + ["xn--1naa7pn51hcbaa.com", "\u0262\u1d0f\u1d0f\u0262\u029f\u1d07.com", kUnsafe], + // Padlock icon spoof. + ["xn--google-hj64e.com", "\U0001f512google.com", kUnsafe], + + // Custom block list + // Combining Long Solidus Overlay + ["google.xn--comabc-k8d", "google.com\u0338abc", kUnsafe], + // Hyphenation Point instead of Katakana Middle dot + ["xn--svgy16dha.jp", "\u30a1\u2027\u30a3.jp", kUnsafe], + // Gershayim with other Hebrew characters is allowed. + ["xn--5db6bh9b.il", "\u05e9\u05d1\u05f4\u05e6.il", kSafe, "DISABLED"], + // Hebrew Gershayim with Latin is invalid according to Python's idna + // package. + ["xn--ab-yod.com", "a\u05f4b.com", kInvalid], + // Hebrew Gershayim with Arabic is disallowed. + ["xn--5eb7h.eg", "\u0628\u05f4.eg", kUnsafe], +// #if BUILDFLAG(IS_APPLE) + // These characters are blocked due to a font issue on Mac. + // Tibetan transliteration characters. + ["xn--com-lum.test.pl", "com\u0f8c.test.pl", kUnsafe], + // Arabic letter KASHMIRI YEH + ["xn--fgb.com", "\u0620.com", kUnsafe, "DISABLED"], +// #endif + + // Hyphens (http://unicode.org/cldr/utility/confusables.jsp?a=-) + // Hyphen-Minus (the only hyphen allowed) + // abc-def + ["abc-def.com", "abc-def.com", kSafe], + // Modifier Letter Minus Sign + ["xn--abcdef-5od.com", "abc\u02d7def.com", kUnsafe], + // Hyphen + ["xn--abcdef-dg0c.com", "abc\u2010def.com", kUnsafe], + // Non-Breaking Hyphen + // This is actually an invalid IDNA domain (U+2011 normalizes to U+2010), + // but it is included to ensure that we do not inadvertently allow this + // character to be displayed as Unicode. + ["xn--abcdef-kg0c.com", "abc\u2011def.com", kInvalid], + // Figure Dash. + // Python's idna package refuses to decode the minus signs and dashes. ICU + // decodes them but treats them as unsafe in spoof checks, so these test + // cases are marked as unsafe instead of invalid. + ["xn--abcdef-rg0c.com", "abc\u2012def.com", kUnsafe], + // En Dash + ["xn--abcdef-yg0c.com", "abc\u2013def.com", kUnsafe], + // Hyphen Bullet + ["xn--abcdef-kq0c.com", "abc\u2043def.com", kUnsafe], + // Minus Sign + ["xn--abcdef-5d3c.com", "abc\u2212def.com", kUnsafe], + // Heavy Minus Sign + ["xn--abcdef-kg1d.com", "abc\u2796def.com", kUnsafe], + // Em Dash + // Small Em Dash (U+FE58) is normalized to Em Dash. + ["xn--abcdef-5g0c.com", "abc\u2014def.com", kUnsafe], + // Coptic Small Letter Dialect-P Ni. Looks like dash. + // Coptic Capital Letter Dialect-P Ni is normalized to small letter. + ["xn--abcdef-yy8d.com", "abc\u2cbbdef.com", kUnsafe], + + // Block NV8 (Not valid in IDN 2008) characters. + // U+058A (֊) + ["xn--ab-vfd.com", "a\u058ab.com", kUnsafe], + ["xn--y9ac3j.com", "\u0561\u058a\u0562.com", kUnsafe], + // U+2019 (’) + ["xn--ab-n2t.com", "a\u2019b.com", kUnsafe], + // U+2027 (‧) + ["xn--ab-u3t.com", "a\u2027b.com", kUnsafe], + // U+30A0 (゠) + ["xn--ab-bg4a.com", "a\u30a0b.com", kUnsafe], + ["xn--9bk3828aea.com", "\uac00\u30a0\uac01.com", kUnsafe], + ["xn--9bk279fba.com", "\u4e00\u30a0\u4e00.com", kUnsafe], + ["xn--n8jl2x.com", "\u304a\u30a0\u3044.com", kUnsafe], + ["xn--fbke7f.com", "\u3082\u30a0\u3084.com", kUnsafe], + + // Block single/double-quote-like characters. + // U+02BB (ʻ) + ["xn--ab-8nb.com", "a\u02bbb.com", kUnsafe, "DISABLED"], + // U+02BC (ʼ) + ["xn--ab-cob.com", "a\u02bcb.com", kUnsafe, "DISABLED"], + // U+144A: Not allowed to mix with scripts other than Canadian Syllabics. + ["xn--ab-jom.com", "a\u144ab.com", kUnsafe], + ["xn--xcec9s.com", "\u1401\u144a\u1402.com", kUnsafe], + + // Custom dangerous patterns + // Two Katakana-Hiragana combining mark in a row + ["google.xn--com-oh4ba.evil.jp", "google.com\u309a\u309a.evil.jp", kUnsafe], + // Katakana Letter No not enclosed by {Han,Hiragana,Katakana}. + ["google.xn--comevil-v04f.jp", "google.com\u30ceevil.jp", kUnsafe, "DISABLED"], + // TODO(jshin): Review the danger of allowing the following two. + // Hiragana 'No' by itself is allowed. + ["xn--ldk.jp", "\u30ce.jp", kSafe], + // Hebrew Gershayim used by itself is allowed. + ["xn--5eb.il", "\u05f4.il", kSafe, "DISABLED"], + + // Block RTL nonspacing marks (NSM) after unrelated scripts. + ["xn--foog-ycg.com", "foog\u0650.com", kUnsafe], // Latin + Arabic N]M + ["xn--foog-jdg.com", "foog\u0654.com", kUnsafe], // Latin + Arabic N]M + ["xn--foog-jhg.com", "foog\u0670.com", kUnsafe], // Latin + Arbic N]M + ["xn--foog-opf.com", "foog\u05b4.com", kUnsafe], // Latin + Hebrew N]M + ["xn--shb5495f.com", "\uac00\u0650.com", kUnsafe], // Hang + Arabic N]M + + // 4 Deviation characters between IDNA 2003 and IDNA 2008 + // When entered in Unicode, the first two are mapped to 'ss' and Greek sigma + // and the latter two are mapped away. However, the punycode form should + // remain in punycode. + // U+00DF(sharp-s) + ["xn--fu-hia.de", "fu\u00df.de", kUnsafe, "DISABLED"], + // U+03C2(final-sigma) + ["xn--mxac2c.gr", "\u03b1\u03b2\u03c2.gr", kUnsafe, "DISABLED"], + // U+200C(ZWNJ) + ["xn--h2by8byc123p.in", "\u0924\u094d\u200c\u0930\u093f.in", kUnsafe], + // U+200C(ZWJ) + ["xn--11b6iy14e.in", "\u0915\u094d\u200d.in", kUnsafe], + + // Math Monospace Small A. When entered in Unicode, it's canonicalized to + // 'a'. The punycode form should remain in punycode. + ["xn--bc-9x80a.xyz", "\U0001d68abc.xyz", kInvalid], + // Math Sans Bold Capital Alpha + ["xn--bc-rg90a.xyz", "\U0001d756bc.xyz", kInvalid], + // U+3000 is canonicalized to a space(U+0020), but the punycode form + // should remain in punycode. + ["xn--p6j412gn7f.cn", "\u4e2d\u56fd\u3000", kInvalid], + // U+3002 is canonicalized to ASCII fullstop(U+002E), but the punycode form + // should remain in punycode. + ["xn--r6j012gn7f.cn", "\u4e2d\u56fd\u3002", kInvalid], + // Invalid punycode + // Has a codepoint beyond U+10FFFF. + ["xn--krank-kg706554a", "", kInvalid], + // '?' in punycode. + ["xn--hello?world.com", "", kInvalid], + + // Not allowed in UTS46/IDNA 2008 + // Georgian Capital Letter(U+10BD) + ["xn--1nd.com", "\u10bd.com", kInvalid], + // 3rd and 4th characters are '-'. + ["xn-----8kci4dhsd", "\u0440\u0443--\u0430\u0432\u0442\u043e", kInvalid], + // Leading combining mark + ["xn--72b.com", "\u093e.com", kInvalid], + // BiDi check per IDNA 2008/UTS 46 + // Cannot starts with AN(Arabic-Indic Number) + ["xn--8hbae.eg", "\u0662\u0660\u0660.eg", kInvalid], + // Cannot start with a RTL character and ends with a LTR + ["xn--x-ymcov.eg", "\u062c\u0627\u0631x.eg", kInvalid], + // Can start with a RTL character and ends with EN(European Number) + ["xn--2-ymcov.eg", "\u062c\u0627\u06312.eg", kSafe], + // Can start with a RTL and end with AN + ["xn--mgbjq0r.eg", "\u062c\u0627\u0631\u0662.eg", kSafe], + + // Extremely rare Latin letters + // Latin Ext B - Pinyin: ǔnion.com + ["xn--nion-unb.com", "\u01d4nion.com", kUnsafe, "DISABLED"], + // Latin Ext C: ⱴase.com + ["xn--ase-7z0b.com", "\u2c74ase.com", kUnsafe], + // Latin Ext D: ꝴode.com + ["xn--ode-ut3l.com", "\ua774ode.com", kUnsafe], + // Latin Ext Additional: ḷily.com + ["xn--ily-n3y.com", "\u1e37ily.com", kUnsafe, "DISABLED"], + // Latin Ext E: ꬺove.com + ["xn--ove-8y6l.com", "\uab3aove.com", kUnsafe], + // Greek Ext: ᾳβγ.com + ["xn--nxac616s.com", "\u1fb3\u03b2\u03b3.com", kInvalid], + // Cyrillic Ext A (label cannot begin with an illegal combining character). + ["xn--lrj.com", "\u2def.com", kInvalid], + // Cyrillic Ext B: ꙡ.com + ["xn--kx8a.com", "\ua661.com", kUnsafe], + // Cyrillic Ext C: ᲂ.com (Narrow o) + ["xn--43f.com", "\u1c82.com", kInvalid], + + // The skeleton of Extended Arabic-Indic Digit Zero (۰) is a dot. Check that + // this is handled correctly (crbug/877045). + ["xn--dmb", "\u06f0", kSafe], + + // Test that top domains whose skeletons are the same as the domain name are + // handled properly. In this case, tést.net should match test.net top + // domain and not be converted to unicode. + ["xn--tst-bma.net", "t\u00e9st.net", kUnsafe, "DISABLED"], + // Variations of the above, for testing crbug.com/925199. + // some.tést.net should match test.net. + ["some.xn--tst-bma.net", "some.t\u00e9st.net", kUnsafe, "DISABLED"], + // The following should not match test.net, so should be converted to + // unicode. + // ést.net (a suffix of tést.net). + ["xn--st-9ia.net", "\u00e9st.net", kSafe], + // some.ést.net + ["some.xn--st-9ia.net", "some.\u00e9st.net", kSafe], + // atést.net (tést.net is a suffix of atést.net) + ["xn--atst-cpa.net", "at\u00e9st.net", kSafe], + // some.atést.net + ["some.xn--atst-cpa.net", "some.at\u00e9st.net", kSafe], + + // Modifier-letter-voicing should be blocked (wwwˬtest.com). + ["xn--wwwtest-2be.com", "www\u02ectest.com", kUnsafe, "DISABLED"], + + // oĸ.com: Not a top domain, should be blocked because of Kra. + ["xn--o-tka.com", "o\u0138.com", kUnsafe], + + // U+4E00 and U+3127 should be blocked when next to non-CJK. + ["xn--ipaddress-w75n.com", "ip\u4e00address.com", kUnsafe, "DISABLED"], + ["xn--ipaddress-wx5h.com", "ip\u3127address.com", kUnsafe, "DISABLED"], + // U+4E00 and U+3127 at the beginning and end of a string. + ["xn--google-gg5e.com", "google\u3127.com", kUnsafe, "DISABLED"], + ["xn--google-9f5e.com", "\u3127google.com", kUnsafe, "DISABLED"], + ["xn--google-gn7i.com", "google\u4e00.com", kUnsafe, "DISABLED"], + ["xn--google-9m7i.com", "\u4e00google.com", kUnsafe, "DISABLED"], + // These are allowed because U+4E00 and U+3127 are not immediately next to + // non-CJK. + ["xn--gamer-fg1hz05u.com", "\u4e00\u751fgamer.com", kSafe], + ["xn--gamer-kg1hy05u.com", "gamer\u751f\u4e00.com", kSafe], + ["xn--gamer-f94d4426b.com", "\u3127\u751fgamer.com", kSafe], + ["xn--gamer-k94d3426b.com", "gamer\u751f\u3127.com", kSafe], + ["xn--4gqz91g.com", "\u4e00\u732b.com", kSafe], + ["xn--4fkv10r.com", "\u3127\u732b.com", kSafe], + // U+4E00 with another ideograph. + ["xn--4gqc.com", "\u4e00\u4e01.com", kSafe], + + // CJK ideographs looking like slashes should be blocked when next to + // non-CJK. + ["example.xn--comtest-k63k", "example.com\u4e36test", kUnsafe, "DISABLED"], + ["example.xn--comtest-u83k", "example.com\u4e40test", kUnsafe, "DISABLED"], + ["example.xn--comtest-283k", "example.com\u4e41test", kUnsafe, "DISABLED"], + ["example.xn--comtest-m83k", "example.com\u4e3ftest", kUnsafe, "DISABLED"], + // This is allowed because the ideographs are not immediately next to + // non-CJK. + ["xn--oiqsace.com", "\u4e36\u4e40\u4e41\u4e3f.com", kSafe], + + // Kana voiced sound marks are not allowed. + ["xn--google-1m4e.com", "google\u3099.com", kUnsafe], + ["xn--google-8m4e.com", "google\u309A.com", kUnsafe], + + // Small letter theta looks like a zero. + ["xn--123456789-yzg.com", "123456789\u03b8.com", kUnsafe, "DISABLED"], + + ["xn--est-118d.net", "\u4e03est.net", kUnsafe, "DISABLED"], + ["xn--est-918d.net", "\u4e05est.net", kUnsafe, "DISABLED"], + ["xn--est-e28d.net", "\u4e06est.net", kUnsafe, "DISABLED"], + ["xn--est-t18d.net", "\u4e01est.net", kUnsafe, "DISABLED"], + ["xn--3-cq6a.com", "\u4e293.com", kUnsafe, "DISABLED"], + ["xn--cxe-n68d.com", "c\u4e2bxe.com", kUnsafe, "DISABLED"], + ["xn--cye-b98d.com", "cy\u4e42e.com", kUnsafe, "DISABLED"], + + // U+05D7 can look like Latin n in many fonts. + ["xn--ceba.com", "\u05d7\u05d7.com", kUnsafe, "DISABLED"], + + // U+00FE (þ) and U+00F0 (ð) are only allowed under the .is TLD. + ["xn--acdef-wva.com", "a\u00fecdef.com", kUnsafe, "DISABLED"], + ["xn--mnpqr-jta.com", "mn\u00f0pqr.com", kUnsafe, "DISABLED"], + ["xn--acdef-wva.is", "a\u00fecdef.is", kSafe], + ["xn--mnpqr-jta.is", "mn\u00f0pqr.is", kSafe], + + // U+0259 (ə) is only allowed under the .az TLD. + ["xn--xample-vyc.com", "\u0259xample.com", kUnsafe, "DISABLED"], + ["xn--xample-vyc.az", "\u0259xample.az", kSafe], + + // U+00B7 is only allowed on Catalan domains between two l's. + ["xn--googlecom-5pa.com", "google\u00b7com.com", kUnsafe, "DISABLED"], + ["xn--ll-0ea.com", "l\u00b7l.com", kUnsafe, "DISABLED"], + ["xn--ll-0ea.cat", "l\u00b7l.cat", kSafe], + ["xn--al-0ea.cat", "a\u00b7l.cat", kUnsafe, "DISABLED"], + ["xn--la-0ea.cat", "l\u00b7a.cat", kUnsafe, "DISABLED"], + ["xn--l-fda.cat", "\u00b7l.cat", kUnsafe, "DISABLED"], + ["xn--l-gda.cat", "l\u00b7.cat", kUnsafe, "DISABLED"], + + ["xn--googlecom-gk6n.com", "google\u4e28com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-0y6n.com", "google\u4e5bcom.com", kUnsafe, "DISABLED"], + ["xn--googlecom-v85n.com", "google\u4e03com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-g95n.com", "google\u4e05com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-go6n.com", "google\u4e36com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-b76o.com", "google\u5341com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-ql3h.com", "google\u3007com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-0r5h.com", "google\u3112com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-bu5h.com", "google\u311acom.com", kUnsafe, "DISABLED"], + ["xn--googlecom-qv5h.com", "google\u311fcom.com", kUnsafe, "DISABLED"], + ["xn--googlecom-0x5h.com", "google\u3127com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-by5h.com", "google\u3128com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-ly5h.com", "google\u3129com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-5o5h.com", "google\u3108com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-075n.com", "google\u4e00com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-046h.com", "google\u31bacom.com", kUnsafe, "DISABLED"], + ["xn--googlecom-026h.com", "google\u31b3com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-lg9q.com", "google\u5de5com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-g040a.com", "google\u8ba0com.com", kUnsafe, "DISABLED"], + ["xn--googlecom-b85n.com", "google\u4e01com.com", kUnsafe, "DISABLED"], + + // Whole-script-confusables. Cyrillic is sufficiently handled in cases above + // so it's not included here. + // Armenian: + ["xn--mbbkpm.com", "\u0578\u057d\u0582\u0585.com", kUnsafe, "DISABLED"], + ["xn--mbbkpm.am", "\u0578\u057d\u0582\u0585.am", kSafe], + ["xn--mbbkpm.xn--y9a3aq", "\u0578\u057d\u0582\u0585.\u0570\u0561\u0575", kSafe], + // Ethiopic: + ["xn--6xd66aa62c.com", "\u1220\u12d0\u12d0\u1350.com", kUnsafe, "DISABLED"], + ["xn--6xd66aa62c.et", "\u1220\u12d0\u12d0\u1350.et", kSafe], + ["xn--6xd66aa62c.xn--m0d3gwjla96a", "\u1220\u12d0\u12d0\u1350.\u12a2\u1275\u12ee\u1335\u12eb", kSafe], + // Greek: + ["xn--mxapd.com", "\u03b9\u03ba\u03b1.com", kUnsafe, "DISABLED"], + ["xn--mxapd.gr", "\u03b9\u03ba\u03b1.gr", kSafe], + ["xn--mxapd.xn--qxam", "\u03b9\u03ba\u03b1.\u03b5\u03bb", kSafe], + // Georgian: + ["xn--gpd3ag.com", "\u10fd\u10ff\u10ee.com", kUnsafe, "DISABLED"], + ["xn--gpd3ag.ge", "\u10fd\u10ff\u10ee.ge", kSafe], + ["xn--gpd3ag.xn--node", "\u10fd\u10ff\u10ee.\u10d2\u10d4", kSafe], + // Hebrew: + ["xn--7dbh4a.com", "\u05d7\u05e1\u05d3.com", kUnsafe, "DISABLED"], + ["xn--7dbh4a.il", "\u05d7\u05e1\u05d3.il", kSafe], + ["xn--9dbq2a.xn--7dbh4a", "\u05e7\u05d5\u05dd.\u05d7\u05e1\u05d3", kSafe], + // Myanmar: + ["xn--oidbbf41a.com", "\u1004\u1040\u1002\u1001\u1002.com", kUnsafe, "DISABLED"], + ["xn--oidbbf41a.mm", "\u1004\u1040\u1002\u1001\u1002.mm", kSafe], + ["xn--oidbbf41a.xn--7idjb0f4ck", "\u1004\u1040\u1002\u1001\u1002.\u1019\u103c\u1014\u103a\u1019\u102c", kSafe], + // Myanmar Shan digits: + ["xn--rmdcmef.com", "\u1090\u1091\u1095\u1096\u1097.com", kUnsafe, "DISABLED"], + ["xn--rmdcmef.mm", "\u1090\u1091\u1095\u1096\u1097.mm", kSafe], + ["xn--rmdcmef.xn--7idjb0f4ck", "\u1090\u1091\u1095\u1096\u1097.\u1019\u103c\u1014\u103a\u1019\u102c", kSafe], +// Thai: +// #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) + ["xn--o3cedqz2c.com", "\u0e17\u0e19\u0e1a\u0e1e\u0e23\u0e2b.com", kUnsafe, "DISABLED"], + ["xn--o3cedqz2c.th", "\u0e17\u0e19\u0e1a\u0e1e\u0e23\u0e2b.th", kSafe], + ["xn--o3cedqz2c.xn--o3cw4h", "\u0e17\u0e19\u0e1a\u0e1e\u0e23\u0e2b.\u0e44\u0e17\u0e22", kSafe], +// #else + ["xn--r3ch7hsc.com", "\u0e1e\u0e1a\u0e40\u0e50.com", kUnsafe, "DISABLED"], + ["xn--r3ch7hsc.th", "\u0e1e\u0e1a\u0e40\u0e50.th", kSafe], + ["xn--r3ch7hsc.xn--o3cw4h", "\u0e1e\u0e1a\u0e40\u0e50.\u0e44\u0e17\u0e22", kSafe], +// #endif + + // Indic scripts: + // Bengali: + ["xn--07baub.com", "\u09e6\u09ed\u09e6\u09ed.com", kUnsafe, "DISABLED"], + // Devanagari: + ["xn--62ba6j.com", "\u093d\u0966\u093d.com", kUnsafe, "DISABLED"], + // Gujarati: + ["xn--becd.com", "\u0aa1\u0a9f.com", kUnsafe, "DISABLED"], + // Gurmukhi: + ["xn--occacb.com", "\u0a66\u0a67\u0a66\u0a67.com", kUnsafe, "DISABLED"], + // Kannada: + ["xn--stca6jf.com", "\u0cbd\u0ce6\u0cbd\u0ce7.com", kUnsafe, "DISABLED"], + // Malayalam: + ["xn--lwccv.com", "\u0d1f\u0d20\u0d27.com", kUnsafe, "DISABLED"], + // Oriya: + ["xn--zhca6ub.com", "\u0b6e\u0b20\u0b6e\u0b20.com", kUnsafe, "DISABLED"], + // Tamil: + ["xn--mlca6ab.com", "\u0b9f\u0baa\u0b9f\u0baa.com", kUnsafe, "DISABLED"], + // Telugu: + ["xn--brcaabbb.com", "\u0c67\u0c66\u0c67\u0c66\u0c67\u0c66.com", kUnsafe, "DISABLED"], + + // IDN domain matching an IDN top-domain (f\u00f3\u00f3.com) + ["xn--fo-5ja.com", "f\u00f3o.com", kUnsafe, "DISABLED"], + + // crbug.com/769547: Subdomains of top domains should be allowed. + ["xn--xample-9ua.test.net", "\u00e9xample.test.net", kSafe], + // Skeleton of the eTLD+1 matches a top domain, but the eTLD+1 itself is + // not a top domain. Should not be decoded to unicode. + ["xn--xample-9ua.test.xn--nt-bja", "\u00e9xample.test.n\u00e9t", kUnsafe, "DISABLED"], + + // Digit lookalike check of 16კ.com with character “კ” (U+10D9) + // Test case for https://crbug.com/1156531 + ["xn--16-1ik.com", "16\u10d9.com", kUnsafe, "DISABLED"], + + // Skeleton generator check of officeკ65.com with character “კ” (U+10D9) + // Test case for https://crbug.com/1156531 + ["xn--office65-l04a.com", "office\u10d965.com", kUnsafe], + + // Digit lookalike check of 16ੜ.com with character “ੜ” (U+0A5C) + // Test case for https://crbug.com/1156531 (missed skeleton map) + ["xn--16-ogg.com", "16\u0a5c.com", kUnsafe, "DISABLED"], + + // Skeleton generator check of officeੜ65.com with character “ੜ” (U+0A5C) + // Test case for https://crbug.com/1156531 (missed skeleton map) + ["xn--office65-hts.com", "office\u0a5c65.com", kUnsafe], + + // New test cases go ↑↑ above. + + // /!\ WARNING: You MUST use tools/security/idn_test_case_generator.py to + // generate new test cases, as specified by the comment at the top of this + // test list. Why must you use that python script? + // 1. It is easy to get things wrong. There were several hand-crafted + // incorrect test cases committed that was later fixed. + // 2. This test _also_ is a test of Chromium's IDN encoder/decoder, so using + // Chromium's IDN encoder/decoder to generate test files loses an + // advantage of having Python's IDN encode/decode the tests. +]; + +function checkEquals(a, b, message, expectedFail) { + if (!expectedFail) { + Assert.equal(a, b, message); + } else { + Assert.notEqual(a, b, `EXPECTED-FAIL: ${message}`); + } +} + +add_task(async function test_chrome_spoofs() { + for (let test of testCases) { + let isAscii = {}; + let result = idnService.convertToDisplayIDN(test[0], isAscii); + let expectedFail = test.length == 4 && test[3] == "DISABLED"; + if (test[2] == kSafe) { + checkEquals( + result, + test[1], + `kSafe label ${test[0]} should convert to ${test[1]}`, + expectedFail + ); + } else if (test[2] == kUnsafe) { + checkEquals( + result, + test[0], + `kUnsafe label ${test[0]} should not convert to ${test[1]}`, + expectedFail + ); + } else if (test[2] == kInvalid) { + checkEquals( + result, + test[0], + `kInvalid label ${test[0]} should stay the same`, + expectedFail + ); + } + } +}); diff --git a/netwerk/test/unit/test_idn_urls.js b/netwerk/test/unit/test_idn_urls.js new file mode 100644 index 0000000000..0059b133c0 --- /dev/null +++ b/netwerk/test/unit/test_idn_urls.js @@ -0,0 +1,436 @@ +// Test algorithm for unicode display of IDNA URL (bug 722299) + +"use strict"; + +const testcases = [ + // Original Punycode or Expected UTF-8 by profile + // URL normalized form ASCII-Only, High, Moderate + // + // Latin script + ["cuillère", "xn--cuillre-6xa", false, true, true], + + // repeated non-spacing marks + ["gruz̀̀ere", "xn--gruzere-ogea", false, false, false], + + // non-XID character + ["I♥NY", "xn--iny-zx5a", false, false, false], + + /* + Behaviour of this test changed in IDNA2008, replacing the non-XID + character with U+FFFD replacement character - when all platforms use + IDNA2008 it can be uncommented and the punycode URL changed to + "xn--mgbl3eb85703a" + + // new non-XID character in Unicode 6.3 + ["حلا\u061cل", "xn--bgbvr6gc", false, false, false], +*/ + + // U+30FB KATAKANA MIDDLE DOT is excluded from non-XID characters (bug 857490) + ["乾燥肌・石けん", "xn--08j4gylj12hz80b0uhfup", false, true, true], + + // Cyrillic alone + ["толсто́й", "xn--lsa83dealbred", false, true, true], + + // Mixed script Cyrillic/Latin + [ + "толсто́й-in-Russian", + "xn---in-russian-1jg071b0a8bb4cpd", + false, + false, + false, + ], + + // Mixed script Latin/Cyrillic + ["war-and-миръ", "xn--war-and--b9g3b7b3h", false, false, false], + + // Cherokee (Restricted script) + ["ᏣᎳᎩ", "xn--f9dt7l", false, false, false], + + // Yi (former Aspirational script, now Restricted per Unicode 10.0 update to UAX 31) + ["ꆈꌠꁱꂷ", "xn--4o7a6e1x64c", false, false, false], + + // Greek alone + ["πλάτων", "xn--hxa3ahjw4a", false, true, true], + + // Mixed script Greek/Latin + [ + "πλάτωνicrelationship", + "xn--icrelationship-96j4t9a3cwe2e", + false, + false, + false, + ], + + // Mixed script Latin/Greek + ["spaceὈδύσσεια", "xn--space-h9dui0b0ga2j1562b", false, false, false], + + // Devanagari alone + ["मराठी", "xn--d2b1ag0dl", false, true, true], + + // Devanagari with Armenian + ["मराठीՀայաստան", "xn--y9aaa1d0ai1cq964f8dwa2o1a", false, false, false], + + // Devanagari with common + ["मराठी123", "xn--123-mhh3em2hra", false, true, true], + + // Common with Devanagari + ["123मराठी", "xn--123-phh3em2hra", false, true, true], + + // Latin with Han + ["chairman毛", "xn--chairman-k65r", false, true, true], + + // Han with Latin + ["山葵sauce", "xn--sauce-6j9ii40v", false, true, true], + + // Latin with Han, Hiragana and Katakana + ["van語ではドイ", "xn--van-ub4bpb6w0in486d", false, true, true], + + // Latin with Han, Katakana and Hiragana + ["van語ドイでは", "xn--van-ub4bpb4w0ip486d", false, true, true], + + // Latin with Hiragana, Han and Katakana + ["vanでは語ドイ", "xn--van-ub4bpb6w0ip486d", false, true, true], + + // Latin with Hiragana, Katakana and Han + ["vanではドイ語", "xn--van-ub4bpb6w0ir486d", false, true, true], + + // Latin with Katakana, Han and Hiragana + ["vanドイ語では", "xn--van-ub4bpb4w0ir486d", false, true, true], + + // Latin with Katakana, Hiragana and Han + ["vanドイでは語", "xn--van-ub4bpb4w0it486d", false, true, true], + + // Han with Latin, Hiragana and Katakana + ["語vanではドイ", "xn--van-ub4bpb6w0ik486d", false, true, true], + + // Han with Latin, Katakana and Hiragana + ["語vanドイでは", "xn--van-ub4bpb4w0im486d", false, true, true], + + // Han with Hiragana, Latin and Katakana + ["語ではvanドイ", "xn--van-rb4bpb9w0ik486d", false, true, true], + + // Han with Hiragana, Katakana and Latin + ["語ではドイvan", "xn--van-rb4bpb6w0in486d", false, true, true], + + // Han with Katakana, Latin and Hiragana + ["語ドイvanでは", "xn--van-ub4bpb1w0ip486d", false, true, true], + + // Han with Katakana, Hiragana and Latin + ["語ドイではvan", "xn--van-rb4bpb4w0ip486d", false, true, true], + + // Hiragana with Latin, Han and Katakana + ["イツvan語ではド", "xn--van-ub4bpb1wvhsbx330n", false, true, true], + + // Hiragana with Latin, Katakana and Han + ["ではvanドイ語", "xn--van-rb4bpb9w0ir486d", false, true, true], + + // Hiragana with Han, Latin and Katakana + ["では語vanドイ", "xn--van-rb4bpb9w0im486d", false, true, true], + + // Hiragana with Han, Katakana and Latin + ["では語ドイvan", "xn--van-rb4bpb6w0ip486d", false, true, true], + + // Hiragana with Katakana, Latin and Han + ["ではドイvan語", "xn--van-rb4bpb6w0iu486d", false, true, true], + + // Hiragana with Katakana, Han and Latin + ["ではドイ語van", "xn--van-rb4bpb6w0ir486d", false, true, true], + + // Katakana with Latin, Han and Hiragana + ["ドイvan語では", "xn--van-ub4bpb1w0iu486d", false, true, true], + + // Katakana with Latin, Hiragana and Han + ["ドイvanでは語", "xn--van-ub4bpb1w0iw486d", false, true, true], + + // Katakana with Han, Latin and Hiragana + ["ドイ語vanでは", "xn--van-ub4bpb1w0ir486d", false, true, true], + + // Katakana with Han, Hiragana and Latin + ["ドイ語ではvan", "xn--van-rb4bpb4w0ir486d", false, true, true], + + // Katakana with Hiragana, Latin and Han + ["ドイではvan語", "xn--van-rb4bpb4w0iw486d", false, true, true], + + // Katakana with Hiragana, Han and Latin + ["ドイでは語van", "xn--van-rb4bpb4w0it486d", false, true, true], + + // Han with common + ["中国123", "xn--123-u68dy61b", false, true, true], + + // common with Han + ["123中国", "xn--123-x68dy61b", false, true, true], + + // Characters that normalize to permitted characters + // (also tests Plane 1 supplementary characters) + ["super𝟖", "super8", true, true, true], + + // Han from Plane 2 + ["𠀀𠀁𠀂", "xn--j50icd", false, true, true], + + // Han from Plane 2 with js (UTF-16) escapes + ["\uD840\uDC00\uD840\uDC01\uD840\uDC02", "xn--j50icd", false, true, true], + + // Same with a lone high surrogate at the end + ["\uD840\uDC00\uD840\uDC01\uD840", "xn--zn7c0336bda", false, false, false], + + // Latin text and Bengali digits + ["super৪", "xn--super-k2l", false, false, true], + + // Bengali digits and Latin text + ["৫ab", "xn--ab-x5f", false, false, true], + + // Bengali text and Latin digits + ["অঙ্কুর8", "xn--8-70d2cp0j6dtd", false, true, true], + + // Latin digits and Bengali text + ["5াব", "xn--5-h3d7c", false, true, true], + + // Mixed numbering systems + ["٢٠۰٠", "xn--8hbae38c", false, false, false], + + // Traditional Chinese + ["萬城", "xn--uis754h", false, true, true], + + // Simplified Chinese + ["万城", "xn--chq31v", false, true, true], + + // Simplified-only and Traditional-only Chinese in the same label + ["万萬城", "xn--chq31vsl1b", false, true, true], + + // Traditional-only and Simplified-only Chinese in the same label + ["萬万城", "xn--chq31vrl1b", false, true, true], + + // Han and Latin and Bopomofo + [ + "注音符号bopomofoㄅㄆㄇㄈ", + "xn--bopomofo-hj5gkalm1637i876cuw0brk5f", + false, + true, + true, + ], + + // Han, bopomofo, Latin + [ + "注音符号ㄅㄆㄇㄈbopomofo", + "xn--bopomofo-8i5gkalm9637i876cuw0brk5f", + false, + true, + true, + ], + + // Latin, Han, Bopomofo + [ + "bopomofo注音符号ㄅㄆㄇㄈ", + "xn--bopomofo-hj5gkalm9637i876cuw0brk5f", + false, + true, + true, + ], + + // Latin, Bopomofo, Han + [ + "bopomofoㄅㄆㄇㄈ注音符号", + "xn--bopomofo-hj5gkalm3737i876cuw0brk5f", + false, + true, + true, + ], + + // Bopomofo, Han, Latin + [ + "ㄅㄆㄇㄈ注音符号bopomofo", + "xn--bopomofo-8i5gkalm3737i876cuw0brk5f", + false, + true, + true, + ], + + // Bopomofo, Latin, Han + [ + "ㄅㄆㄇㄈbopomofo注音符号", + "xn--bopomofo-8i5gkalm1837i876cuw0brk5f", + false, + true, + true, + ], + + // Han, bopomofo and katakana + [ + "注音符号ㄅㄆㄇㄈボポモフォ", + "xn--jckteuaez1shij0450gylvccz9asi4e", + false, + false, + false, + ], + + // Han, katakana, bopomofo + [ + "注音符号ボポモフォㄅㄆㄇㄈ", + "xn--jckteuaez6shij5350gylvccz9asi4e", + false, + false, + false, + ], + + // bopomofo, han, katakana + [ + "ㄅㄆㄇㄈ注音符号ボポモフォ", + "xn--jckteuaez1shij4450gylvccz9asi4e", + false, + false, + false, + ], + + // bopomofo, katakana, han + [ + "ㄅㄆㄇㄈボポモフォ注音符号", + "xn--jckteuaez1shij9450gylvccz9asi4e", + false, + false, + false, + ], + + // katakana, Han, bopomofo + [ + "ボポモフォ注音符号ㄅㄆㄇㄈ", + "xn--jckteuaez6shij0450gylvccz9asi4e", + false, + false, + false, + ], + + // katakana, bopomofo, Han + [ + "ボポモフォㄅㄆㄇㄈ注音符号", + "xn--jckteuaez6shij4450gylvccz9asi4e", + false, + false, + false, + ], + + // Han, Hangul and Latin + ["韓한글hangul", "xn--hangul-2m5ti09k79ze", false, true, true], + + // Han, Latin and Hangul + ["韓hangul한글", "xn--hangul-2m5to09k79ze", false, true, true], + + // Hangul, Han and Latin + ["한글韓hangul", "xn--hangul-2m5th09k79ze", false, true, true], + + // Hangul, Latin and Han + ["한글hangul韓", "xn--hangul-8m5t898k79ze", false, true, true], + + // Latin, Han and Hangul + ["hangul韓한글", "xn--hangul-8m5ti09k79ze", false, true, true], + + // Latin, Hangul and Han + ["hangul한글韓", "xn--hangul-8m5th09k79ze", false, true, true], + + // Hangul and katakana + ["한글ハングル", "xn--qck1c2d4a9266lkmzb", false, false, false], + + // Katakana and Hangul + ["ハングル한글", "xn--qck1c2d4a2366lkmzb", false, false, false], + + // Thai (also tests that node with over 63 UTF-8 octets doesn't fail) + [ + "เครื่องทําน้ําทําน้ําแข็ง", + "xn--22cdjb2fanb9fyepcbbb9dwh4a3igze4fdcd", + false, + true, + true, + ], + + // Effect of adding valid or invalid subdomains (bug 1399540) + ["䕮䕵䕶䕱.ascii", "xn--google.ascii", false, true, true], + ["ascii.䕮䕵䕶䕱", "ascii.xn--google", false, true, true], + ["中国123.䕮䕵䕶䕱", "xn--123-u68dy61b.xn--google", false, true, true], + ["䕮䕵䕶䕱.中国123", "xn--google.xn--123-u68dy61b", false, true, true], + [ + "xn--accountlogin.䕮䕵䕶䕱", + "xn--accountlogin.xn--google", + false, + true, + true, + ], + [ + "䕮䕵䕶䕱.xn--accountlogin", + "xn--google.xn--accountlogin", + false, + true, + true, + ], + + // Arabic diacritic not allowed in Latin text (bug 1370497) + ["goo\u0650gle", "xn--google-yri", false, false, false], + // ...but Arabic diacritics are allowed on Arabic text + ["العَرَبِي", "xn--mgbc0a5a6cxbzabt", false, true, true], + + // Hebrew diacritic also not allowed in Latin text (bug 1404349) + ["goo\u05b4gle", "xn--google-rvh", false, false, false], + + // Accents above dotless-i are not allowed + ["na\u0131\u0308ve", "xn--nave-mza04z", false, false, false], + ["d\u0131\u0302ner", "xn--dner-lza40z", false, false, false], + // but the corresponding accented-i (based on dotted i) is OK + ["na\u00efve.com", "xn--nave-6pa.com", false, true, true], + ["d\u00eener.com", "xn--dner-0pa.com", false, true, true], +]; + +const profiles = ["ASCII", "high", "moderate"]; + +function run_test() { + var pbi = Services.prefs; + var oldProfile = pbi.getCharPref( + "network.IDN.restriction_profile", + "moderate" + ); + var idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + + for (var i = 0; i < profiles.length; ++i) { + pbi.setCharPref("network.IDN.restriction_profile", profiles[i]); + + dump("testing " + profiles[i] + " profile"); + + for (var j = 0; j < testcases.length; ++j) { + var test = testcases[j]; + var URL = test[0] + ".com"; + var punycodeURL = test[1] + ".com"; + var expectedUnicode = test[2 + i]; + var isASCII = {}; + + var result; + try { + result = idnService.convertToDisplayIDN(URL, isASCII); + } catch (e) { + result = ".com"; + } + if ( + punycodeURL.substr(0, 4) == "xn--" || + punycodeURL.indexOf(".xn--") > 0 + ) { + // test convertToDisplayIDN with a Unicode URL and with a + // Punycode URL if we have one + Assert.equal( + escape(result), + expectedUnicode ? escape(URL) : escape(punycodeURL) + ); + + result = idnService.convertToDisplayIDN(punycodeURL, isASCII); + Assert.equal( + escape(result), + expectedUnicode ? escape(URL) : escape(punycodeURL) + ); + } else { + // The "punycode" URL isn't punycode. This happens in testcases + // where the Unicode URL has become normalized to an ASCII URL, + // so, even though expectedUnicode is true, the expected result + // is equal to punycodeURL + Assert.equal(escape(result), escape(punycodeURL)); + } + } + } + pbi.setCharPref("network.IDN.restriction_profile", oldProfile); +} diff --git a/netwerk/test/unit/test_idna2008.js b/netwerk/test/unit/test_idna2008.js new file mode 100644 index 0000000000..0e0290ce79 --- /dev/null +++ b/netwerk/test/unit/test_idna2008.js @@ -0,0 +1,65 @@ +/* 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 kTransitionalProcessing = false; + +// Four characters map differently under non-transitional processing: +const labels = [ + // U+00DF LATIN SMALL LETTER SHARP S to "ss" + "stra\u00dfe", + // U+03C2 GREEK SMALL LETTER FINAL SIGMA to U+03C3 GREEK SMALL LETTER SIGMA + "\u03b5\u03bb\u03bb\u03ac\u03c2", + // U+200C ZERO WIDTH NON-JOINER in Indic script + "\u0646\u0627\u0645\u0647\u200c\u0627\u06cc", + // U+200D ZERO WIDTH JOINER in Arabic script + "\u0dc1\u0dca\u200d\u0dbb\u0dd3", + + // But CONTEXTJ rules prohibit ZWJ and ZWNJ in non-Arabic or Indic scripts + // U+200C ZERO WIDTH NON-JOINER in Latin script + "m\u200cn", + // U+200D ZERO WIDTH JOINER in Latin script + "p\u200dq", +]; + +const transitionalExpected = [ + "strasse", + "xn--hxarsa5b", + "xn--mgba3gch31f", + "xn--10cl1a0b", + "", + "", +]; + +const nonTransitionalExpected = [ + "xn--strae-oqa", + "xn--hxarsa0b", + "xn--mgba3gch31f060k", + "xn--10cl1a0b660p", + "", + "", +]; + +// Test options for converting IDN URLs under IDNA2008 +function run_test() { + var idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + + for (var i = 0; i < labels.length; ++i) { + var result; + try { + result = idnService.convertUTF8toACE(labels[i]); + } catch (e) { + result = ""; + } + + if (kTransitionalProcessing) { + equal(result, transitionalExpected[i]); + } else { + equal(result, nonTransitionalExpected[i]); + } + } +} diff --git a/netwerk/test/unit/test_idnservice.js b/netwerk/test/unit/test_idnservice.js new file mode 100644 index 0000000000..0c52f300e3 --- /dev/null +++ b/netwerk/test/unit/test_idnservice.js @@ -0,0 +1,39 @@ +// Tests nsIIDNService + +"use strict"; + +const idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService +); + +add_task(async function test_simple() { + let reference = [ + // The 3rd element indicates whether the second element + // is ACE-encoded + ["asciihost", "asciihost", false], + ["b\u00FCcher", "xn--bcher-kva", true], + ]; + + for (var i = 0; i < reference.length; ++i) { + dump("Testing " + reference[i] + "\n"); + // We test the following: + // - Converting UTF-8 to ACE and back gives us the expected answer + // - Converting the ASCII string UTF-8 -> ACE leaves the string unchanged + // - isACE returns true when we expect it to (third array elem true) + Assert.equal(idnService.convertUTF8toACE(reference[i][0]), reference[i][1]); + Assert.equal(idnService.convertUTF8toACE(reference[i][1]), reference[i][1]); + Assert.equal(idnService.convertACEtoUTF8(reference[i][1]), reference[i][0]); + Assert.equal(idnService.isACE(reference[i][1]), reference[i][2]); + } +}); + +add_task(async function test_extra_blocked() { + let isAscii = {}; + equal(idnService.convertToDisplayIDN("xn--gou-2lb.ro", isAscii), "goșu.ro"); + Services.prefs.setStringPref("network.IDN.extra_blocked_chars", "ș"); + equal( + idnService.convertToDisplayIDN("xn--gou-2lb.ro", isAscii), + "xn--gou-2lb.ro" + ); + Services.prefs.clearUserPref("network.IDN.extra_blocked_chars"); +}); diff --git a/netwerk/test/unit/test_immutable.js b/netwerk/test/unit/test_immutable.js new file mode 100644 index 0000000000..a3149d0dd8 --- /dev/null +++ b/netwerk/test/unit/test_immutable.js @@ -0,0 +1,160 @@ +"use strict"; + +var prefs; +var http2pref; +var origin; +var rcwnpref; + +function run_test() { + var h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Services.prefs; + + http2pref = prefs.getBoolPref("network.http.http2.enabled"); + rcwnpref = prefs.getBoolPref("network.http.rcwn.enabled"); + + prefs.setBoolPref("network.http.http2.enabled", true); + prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, bar.example.com" + ); + // Disable rcwn to make cache behavior deterministic. + prefs.setBoolPref("network.http.rcwn.enabled", false); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + origin = "https://foo.example.com:" + h2Port; + dump("origin - " + origin + "\n"); + doTest1(); +} + +function resetPrefs() { + prefs.setBoolPref("network.http.http2.enabled", http2pref); + prefs.setBoolPref("network.http.rcwn.enabled", rcwnpref); + prefs.clearUserPref("network.dns.localDomains"); +} + +function makeChan(origin, path) { + return NetUtil.newChannel({ + uri: origin + path, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var nextTest; +var expectPass = true; +var expectConditional = false; + +var Listener = function () {}; +Listener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + if (expectPass) { + if (!Components.isSuccessCode(request.status)) { + do_throw( + "Channel should have a success code! (" + request.status + ")" + ); + } + Assert.equal(request.responseStatus, 200); + } else { + Assert.equal(Components.isSuccessCode(request.status), false); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + if (expectConditional) { + Assert.equal(request.getResponseHeader("x-conditional"), "true"); + } else { + try { + Assert.notEqual(request.getResponseHeader("x-conditional"), "true"); + } catch (e) { + Assert.ok(true); + } + } + nextTest(); + do_test_finished(); + }, +}; + +function testsDone() { + dump("testDone\n"); + resetPrefs(); +} + +function doTest1() { + dump("execute doTest1 - resource without immutable. initial request\n"); + do_test_pending(); + expectConditional = false; + var chan = makeChan(origin, "/immutable-test-without-attribute"); + var listener = new Listener(); + nextTest = doTest2; + chan.asyncOpen(listener); +} + +function doTest2() { + dump("execute doTest2 - resource without immutable. reload\n"); + do_test_pending(); + expectConditional = true; + var chan = makeChan(origin, "/immutable-test-without-attribute"); + var listener = new Listener(); + nextTest = doTest3; + chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS; + chan.asyncOpen(listener); +} + +function doTest3() { + dump("execute doTest3 - resource without immutable. shift reload\n"); + do_test_pending(); + expectConditional = false; + var chan = makeChan(origin, "/immutable-test-without-attribute"); + var listener = new Listener(); + nextTest = doTest4; + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen(listener); +} + +function doTest4() { + dump("execute doTest1 - resource with immutable. initial request\n"); + do_test_pending(); + expectConditional = false; + var chan = makeChan(origin, "/immutable-test-with-attribute"); + var listener = new Listener(); + nextTest = doTest5; + chan.asyncOpen(listener); +} + +function doTest5() { + dump("execute doTest5 - resource with immutable. reload\n"); + do_test_pending(); + expectConditional = false; + var chan = makeChan(origin, "/immutable-test-with-attribute"); + var listener = new Listener(); + nextTest = doTest6; + chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS; + chan.asyncOpen(listener); +} + +function doTest6() { + dump("execute doTest3 - resource with immutable. shift reload\n"); + do_test_pending(); + expectConditional = false; + var chan = makeChan(origin, "/immutable-test-with-attribute"); + var listener = new Listener(); + nextTest = testsDone; + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen(listener); +} diff --git a/netwerk/test/unit/test_inhibit_caching.js b/netwerk/test/unit/test_inhibit_caching.js new file mode 100644 index 0000000000..f23e36f5f2 --- /dev/null +++ b/netwerk/test/unit/test_inhibit_caching.js @@ -0,0 +1,92 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var first = true; +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + var body = "first"; + if (!first) { + body = "second"; + } + first = false; + response.bodyOutputStream.write(body, body.length); +} + +XPCOMUtils.defineLazyGetter(this, "uri", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = null; + +function run_test() { + // setup test + httpserver = new HttpServer(); + httpserver.registerPathHandler("/test", contentHandler); + httpserver.start(-1); + + add_test(test_first_response); + add_test(test_inhibit_caching); + + run_next_test(); +} + +// Makes a regular request +function test_first_response() { + var chan = NetUtil.newChannel({ + uri: uri + "/test", + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen(new ChannelListener(check_first_response, null)); +} + +// Checks that we got the appropriate response +function check_first_response(request, buffer) { + request.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(request.responseStatus, 200); + Assert.equal(buffer, "first"); + // Open the cache entry to check its contents + asyncOpenCacheEntry( + uri + "/test", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + cache_entry_callback + ); +} + +// Checks that the cache entry has the correct contents +function cache_entry_callback(status, entry) { + equal(status, Cr.NS_OK); + var inputStream = entry.openInputStream(0); + pumpReadStream(inputStream, function (read) { + inputStream.close(); + equal(read, "first"); + run_next_test(); + }); +} + +// Makes a request with the INHIBIT_CACHING load flag +function test_inhibit_caching() { + var chan = NetUtil.newChannel({ + uri: uri + "/test", + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIRequest).loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + chan.asyncOpen(new ChannelListener(check_second_response, null)); +} + +// Checks that we got a different response from the first request +function check_second_response(request, buffer) { + request.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(request.responseStatus, 200); + Assert.equal(buffer, "second"); + // Checks that the cache entry still contains the content from the first request + asyncOpenCacheEntry( + uri + "/test", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + cache_entry_callback + ); +} diff --git a/netwerk/test/unit/test_ioservice.js b/netwerk/test/unit/test_ioservice.js new file mode 100644 index 0000000000..d218d77a7a --- /dev/null +++ b/netwerk/test/unit/test_ioservice.js @@ -0,0 +1,19 @@ +"use strict"; + +add_task(function test_extractScheme() { + equal(Services.io.extractScheme("HtTp://example.com"), "http"); + Assert.throws( + () => { + Services.io.extractScheme("://example.com"); + }, + /NS_ERROR_MALFORMED_URI/, + "missing scheme" + ); + Assert.throws( + () => { + Services.io.extractScheme("ht%tp://example.com"); + }, + /NS_ERROR_MALFORMED_URI/, + "bad scheme" + ); +}); diff --git a/netwerk/test/unit/test_large_port.js b/netwerk/test/unit/test_large_port.js new file mode 100644 index 0000000000..a0dd0a19cf --- /dev/null +++ b/netwerk/test/unit/test_large_port.js @@ -0,0 +1,65 @@ +/* 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/. */ + +// Ensure that non-16-bit URIs are rejected + +"use strict"; + +function run_test() { + let mutator = Cc[ + "@mozilla.org/network/standard-url-mutator;1" + ].createInstance(Ci.nsIURIMutator); + Assert.ok(mutator, "Mutator constructor works"); + + let url = Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIStandardURLMutator) + .init( + Ci.nsIStandardURL.URLTYPE_AUTHORITY, + 65535, + "http://localhost", + "UTF-8", + null + ) + .finalize(); + + // Bug 1301621 makes invalid ports throw + Assert.throws( + () => { + url = Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIStandardURLMutator) + .init( + Ci.nsIStandardURL.URLTYPE_AUTHORITY, + 65536, + "http://localhost", + "UTF-8", + null + ) + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "invalid port during creation" + ); + + Assert.throws( + () => { + url = url + .mutate() + .QueryInterface(Ci.nsIStandardURLMutator) + .setDefaultPort(65536) + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "invalid port in setDefaultPort" + ); + Assert.throws( + () => { + url = url.mutate().setPort(65536).finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "invalid port in port setter" + ); + + Assert.equal(url.port, -1); + do_test_finished(); +} diff --git a/netwerk/test/unit/test_link.desktop b/netwerk/test/unit/test_link.desktop new file mode 100644 index 0000000000..b1798202e3 --- /dev/null +++ b/netwerk/test/unit/test_link.desktop @@ -0,0 +1,3 @@ +[Desktop Entry] +Type=Link +URL=http://www.mozilla.org/ diff --git a/netwerk/test/unit/test_link.lnk b/netwerk/test/unit/test_link.lnk Binary files differnew file mode 100644 index 0000000000..125d859f43 --- /dev/null +++ b/netwerk/test/unit/test_link.lnk diff --git a/netwerk/test/unit/test_link.url b/netwerk/test/unit/test_link.url new file mode 100644 index 0000000000..05f8275544 --- /dev/null +++ b/netwerk/test/unit/test_link.url @@ -0,0 +1,5 @@ +[InternetShortcut] +URL=http://www.mozilla.org/ +IDList= +[{000214A0-0000-0000-C000-000000000046}] +Prop3=19,2 diff --git a/netwerk/test/unit/test_loadgroup_cancel.js b/netwerk/test/unit/test_loadgroup_cancel.js new file mode 100644 index 0000000000..accfede36a --- /dev/null +++ b/netwerk/test/unit/test_loadgroup_cancel.js @@ -0,0 +1,94 @@ +/* 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 makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +function request_handler(metadata, response) { + response.processAsync(); + do_timeout(500, () => { + const body = "some body once told me..."; + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Length", "" + body.length, false); + response.bodyOutputStream.write(body, body.length); + response.finish(); + }); +} + +// This test checks that when canceling a loadgroup by the time the loadgroup's +// groupObserver is sent OnStopRequest for a request, that request has been +// canceled. +add_task(async function test_cancelledInOnStop() { + let http_server = new HttpServer(); + http_server.registerPathHandler("/test1", request_handler); + http_server.registerPathHandler("/test2", request_handler); + http_server.registerPathHandler("/test3", request_handler); + http_server.start(-1); + const port = http_server.identity.primaryPort; + + let loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + + let loadListener = { + onStartRequest: aRequest => { + info("onStartRequest"); + }, + onStopRequest: (aRequest, aStatusCode) => { + equal( + aStatusCode, + Cr.NS_ERROR_ABORT, + "aStatusCode must be the cancellation code" + ); + equal( + aRequest.status, + Cr.NS_ERROR_ABORT, + "aRequest.status must be the cancellation code" + ); + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIRequestObserver", + "nsISupportsWeakReference", + ]), + }; + loadGroup.groupObserver = loadListener; + + let chan1 = makeChan(`http://localhost:${port}/test1`); + chan1.loadGroup = loadGroup; + let chan2 = makeChan(`http://localhost:${port}/test2`); + chan2.loadGroup = loadGroup; + let chan3 = makeChan(`http://localhost:${port}/test3`); + chan3.loadGroup = loadGroup; + + await new Promise(resolve => do_timeout(500, resolve)); + + let promises = [ + new Promise(resolve => { + chan1.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)); + }), + new Promise(resolve => { + chan2.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)); + }), + new Promise(resolve => { + chan3.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)); + }), + ]; + + loadGroup.cancel(Cr.NS_ERROR_ABORT); + + await Promise.all(promises); + + await new Promise(resolve => { + http_server.stop(resolve); + }); +}); diff --git a/netwerk/test/unit/test_localhost_offline.js b/netwerk/test/unit/test_localhost_offline.js new file mode 100644 index 0000000000..de0ff6df4e --- /dev/null +++ b/netwerk/test/unit/test_localhost_offline.js @@ -0,0 +1,75 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +var httpServer = null; +const body = "Hello"; + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +function makeURL(host) { + return `http://${host}:${httpServer.identity.primaryPort}/`; +} + +add_task(async function test_localhost_offline() { + Services.io.offline = true; + Services.prefs.setBoolPref("network.disable-localhost-when-offline", false); + let chan = makeChan(makeURL("127.0.0.1")); + let [, resp] = await channelOpenPromise(chan); + Assert.equal(resp, body, "Should get correct response"); + + chan = makeChan(makeURL("localhost")); + [, resp] = await channelOpenPromise(chan); + Assert.equal(resp, body, "Should get response"); + + Services.prefs.setBoolPref("network.disable-localhost-when-offline", true); + + chan = makeChan(makeURL("127.0.0.1")); + let [req] = await channelOpenPromise( + chan, + CL_ALLOW_UNKNOWN_CL | CL_EXPECT_FAILURE + ); + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.status, Cr.NS_ERROR_OFFLINE); + + chan = makeChan(makeURL("localhost")); + [req] = await channelOpenPromise( + chan, + CL_ALLOW_UNKNOWN_CL | CL_EXPECT_FAILURE + ); + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.status, Cr.NS_ERROR_OFFLINE); + + Services.prefs.clearUserPref("network.disable-localhost-when-offline"); + Services.io.offline = false; +}); + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/", (request, response) => { + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Length: " + body.length + "\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); + }); + httpServer.start(-1); + run_next_test(); +} diff --git a/netwerk/test/unit/test_localstreams.js b/netwerk/test/unit/test_localstreams.js new file mode 100644 index 0000000000..3e9c26b111 --- /dev/null +++ b/netwerk/test/unit/test_localstreams.js @@ -0,0 +1,89 @@ +// Tests bug 304414 + +"use strict"; + +const PR_RDONLY = 0x1; // see prio.h + +// Does some sanity checks on the stream and returns the number of bytes read +// when the checks passed. +function test_stream(stream) { + // This test only handles blocking streams; that's desired for file streams + // anyway. + Assert.equal(stream.isNonBlocking(), false); + + // Check that the stream is not buffered + Assert.equal( + Cc["@mozilla.org/io-util;1"] + .getService(Ci.nsIIOUtil) + .inputStreamIsBuffered(stream), + false + ); + + // Wrap it in a binary stream (to avoid wrong results that + // scriptablestream would produce with binary content) + var binstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + binstream.setInputStream(stream); + + var numread = 0; + for (;;) { + Assert.equal(stream.available(), binstream.available()); + var avail = stream.available(); + Assert.notEqual(avail, -1); + + // PR_UINT32_MAX and PR_INT32_MAX; the files we're testing with aren't that + // large. + Assert.notEqual(avail, Math.pow(2, 32) - 1); + Assert.notEqual(avail, Math.pow(2, 31) - 1); + + if (!avail) { + // For blocking streams, available() only returns 0 on EOF + // Make sure that there is really no data left + var could_read = false; + try { + binstream.readByteArray(1); + could_read = true; + } catch (e) { + // We expect the exception, so do nothing here + } + if (could_read) { + do_throw("Data readable when available indicated EOF!"); + } + return numread; + } + + dump("Trying to read " + avail + " bytes\n"); + // Note: Verification that this does return as much bytes as we asked for is + // done in the binarystream implementation + binstream.readByteArray(avail); + + numread += avail; + } +} + +function stream_for_file(file) { + var str = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + str.init(file, PR_RDONLY, 0, 0); + return str; +} + +function stream_from_channel(file) { + var uri = Services.io.newFileURI(file); + return NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).open(); +} + +function run_test() { + // Get a file and a directory in order to do some testing + var file = do_get_file("../unit/data/test_readline6.txt"); + var len = file.fileSize; + Assert.equal(test_stream(stream_for_file(file)), len); + Assert.equal(test_stream(stream_from_channel(file)), len); + var dir = file.parent; + test_stream(stream_from_channel(dir)); // Can't do size checking +} diff --git a/netwerk/test/unit/test_mismatch_last-modified.js b/netwerk/test/unit/test_mismatch_last-modified.js new file mode 100644 index 0000000000..28f9f9642e --- /dev/null +++ b/netwerk/test/unit/test_mismatch_last-modified.js @@ -0,0 +1,152 @@ +"use strict"; + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +var httpserver = new HttpServer(); + +// Test the handling of a cache revalidation with mismatching last-modified +// headers. If we get such a revalidation the cache entry should be purged. +// see bug 717350 + +// In this test the wrong data is from 11-16-1994 with a value of 'A', +// and the right data is from 11-15-1994 with a value of 'B'. + +// the same URL is requested 3 times. the first time the wrong data comes +// back, the second time that wrong data is revalidated with a 304 but +// a L-M header of the right data (this triggers a cache purge), and +// the third time the right data is returned. + +var listener_3 = { + // this listener is used to process the the request made after + // the cache invalidation. it expects to see the 'right data' + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest: function test_onStartR(request) {}, + + onDataAvailable: function test_ODA(request, inputStream, offset, count) { + var data = new BinaryInputStream(inputStream).readByteArray(count); + + Assert.equal(data[0], "B".charCodeAt(0)); + }, + + onStopRequest: function test_onStopR(request, status) { + httpserver.stop(do_test_finished); + }, +}; + +XPCOMUtils.defineLazyGetter(this, "listener_2", function () { + return { + // this listener is used to process the revalidation of the + // corrupted cache entry. its revalidation prompts it to be cleaned + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest: function test_onStartR(request) {}, + + onDataAvailable: function test_ODA(request, inputStream, offset, count) { + var data = new BinaryInputStream(inputStream).readByteArray(count); + + // This is 'A' from a cache revalidation, but that reval will clean the cache + // because of mismatched last-modified response headers + + Assert.equal(data[0], "A".charCodeAt(0)); + }, + + onStopRequest: function test_onStopR(request, status) { + request.QueryInterface(Ci.nsIHttpChannel); + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + "/test1", + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen(listener_3); + }, + }; +}); + +XPCOMUtils.defineLazyGetter(this, "listener_1", function () { + return { + // this listener processes the initial request from a empty cache. + // the server responds with the wrong data ('A') + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest: function test_onStartR(request) {}, + + onDataAvailable: function test_ODA(request, inputStream, offset, count) { + var data = new BinaryInputStream(inputStream).readByteArray(count); + Assert.equal(data[0], "A".charCodeAt(0)); + }, + + onStopRequest: function test_onStopR(request, status) { + request.QueryInterface(Ci.nsIHttpChannel); + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + "/test1", + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen(listener_2); + }, + }; +}); + +function run_test() { + do_get_profile(); + + evict_cache_entries(); + + httpserver.registerPathHandler("/test1", handler); + httpserver.start(-1); + + var port = httpserver.identity.primaryPort; + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + port + "/test1", + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen(listener_1); + + do_test_pending(); +} + +var iter = 0; +function handler(metadata, response) { + iter++; + if (metadata.hasHeader("If-Modified-Since")) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + response.setHeader("Last-Modified", "Tue, 15 Nov 1994 12:45:26 GMT", false); + } else { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "max-age=0", false); + if (iter == 1) { + // simulated wrong response + response.setHeader( + "Last-Modified", + "Wed, 16 Nov 1994 00:00:00 GMT", + false + ); + response.bodyOutputStream.write("A", 1); + } + if (iter == 3) { + // 'correct' response + response.setHeader( + "Last-Modified", + "Tue, 15 Nov 1994 12:45:26 GMT", + false + ); + response.bodyOutputStream.write("B", 1); + } + } +} diff --git a/netwerk/test/unit/test_mozTXTToHTMLConv.js b/netwerk/test/unit/test_mozTXTToHTMLConv.js new file mode 100644 index 0000000000..1e93a440ad --- /dev/null +++ b/netwerk/test/unit/test_mozTXTToHTMLConv.js @@ -0,0 +1,394 @@ +/* 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/. */ + +/** + * Test that mozITXTToHTMLConv works properly. + */ + +"use strict"; + +function run_test() { + let converter = Cc["@mozilla.org/txttohtmlconv;1"].getService( + Ci.mozITXTToHTMLConv + ); + + const scanTXTtests = [ + // -- RFC1738 + { + input: "RFC1738: <URL:http://mozilla.org> then", + url: "http://mozilla.org", + }, + { + input: "RFC1738: <URL:mailto:john.doe+test@mozilla.org> then", + url: "mailto:john.doe+test@mozilla.org", + }, + // -- RFC2396E + { + input: "RFC2396E: <http://mozilla.org/> then", + url: "http://mozilla.org/", + }, + { + input: "RFC2396E: <john.doe+test@mozilla.org> then", + url: "mailto:john.doe+test@mozilla.org", + }, + // -- abbreviated + { + input: "see www.mozilla.org maybe", + url: "http://www.mozilla.org", + }, + { + input: "mail john.doe+test@mozilla.org maybe", + url: "mailto:john.doe+test@mozilla.org", + }, + // -- delimiters + { + input: "see http://www.mozilla.org/maybe today", // Spaces + url: "http://www.mozilla.org/maybe", + }, + { + input: 'see "http://www.mozilla.org/maybe today"', // Double quotes + url: "http://www.mozilla.org/maybetoday", // spaces ignored + }, + { + input: "see <http://www.mozilla.org/maybe today>", // Angle brackets + url: "http://www.mozilla.org/maybetoday", // spaces ignored + }, + // -- freetext + { + input: "I mean http://www.mozilla.org/.", + url: "http://www.mozilla.org/", + }, + { + input: "you mean http://mozilla.org:80, right?", + url: "http://mozilla.org:80", + }, + { + input: "go to http://mozilla.org; then go home", + url: "http://mozilla.org", + }, + { + input: "http://mozilla.org! yay!", + url: "http://mozilla.org", + }, + { + input: "er, http://mozilla.com?", + url: "http://mozilla.com", + }, + { + input: "http://example.org- where things happen", + url: "http://example.org", + }, + { + input: "see http://mozilla.org: front page", + url: "http://mozilla.org", + }, + { + input: "'http://mozilla.org/': that's the url", + url: "http://mozilla.org/", + }, + { + input: "some special http://mozilla.org/?x=.,;!-:x", + url: "http://mozilla.org/?x=.,;!-:x", + }, + { + // escape & when producing html + input: "'http://example.org/?test=true&success=true': ok", + url: "http://example.org/?test=true&success=true", + }, + { + input: "bracket: http://localhost/[1] etc.", + url: "http://localhost/", + }, + { + input: "bracket: john.doe+test@mozilla.org[1] etc.", + url: "mailto:john.doe+test@mozilla.org", + }, + { + input: "parenthesis: (http://localhost/) etc.", + url: "http://localhost/", + }, + { + input: "parenthesis: (john.doe+test@mozilla.org) etc.", + url: "mailto:john.doe+test@mozilla.org", + }, + { + input: "(thunderbird)http://mozilla.org/thunderbird", + url: "http://mozilla.org/thunderbird", + }, + { + input: "(mail)john.doe+test@mozilla.org", + url: "mailto:john.doe+test@mozilla.org", + }, + { + input: "()http://mozilla.org", + url: "http://mozilla.org", + }, + { + input: + "parenthesis included: http://kb.mozillazine.org/Performance_(Thunderbird) etc.", + url: "http://kb.mozillazine.org/Performance_(Thunderbird)", + }, + { + input: "parenthesis slash bracket: (http://localhost/)[1] etc.", + url: "http://localhost/", + }, + { + input: "parenthesis bracket: (http://example.org[1]) etc.", + url: "http://example.org", + }, + { + input: "ipv6 1: https://[1080::8:800:200C:417A]/foo?bar=x test", + url: "https://[1080::8:800:200C:417A]/foo?bar=x", + }, + { + input: "ipv6 2: http://[::ffff:127.0.0.1]/#yay test", + url: "http://[::ffff:127.0.0.1]/#yay", + }, + { + input: "ipv6 parenthesis port: (http://[2001:db8::1]:80/) test", + url: "http://[2001:db8::1]:80/", + }, + { + input: + "test http://www.map.com/map.php?t=Nova_Scotia&markers=//Not_a_survey||description=plm2 test", + url: "http://www.map.com/map.php?t=Nova_Scotia&markers=//Not_a_survey||description=plm2", + }, + { + input: "bug#1509493 (john@mozilla.org)john@mozilla.org test", + url: "mailto:john@mozilla.org", + text: "john@mozilla.org", + }, + { + input: "bug#1509493 {john@mozilla.org}john@mozilla.org test", + url: "mailto:john@mozilla.org", + text: "john@mozilla.org", + }, + ]; + + const scanTXTglyph = [ + // Some "glyph" testing (not exhaustive, the system supports 16 different + // smiley types). + { + input: "this is superscript: x^2", + results: ["<sup", "2", "</sup>"], + }, + { + input: "this is plus-minus: +/-", + results: ["±"], + }, + { + input: "this is a smiley :)", + results: ["🙂"], + }, + { + input: "this is a smiley :-)", + results: ["🙂"], + }, + { + input: "this is a smiley :-(", + results: ["🙁"], + }, + ]; + + const scanTXTstrings = [ + "underline", // ASCII + "äöüßáéíóúî", // Latin-1 + "a\u0301c\u0327c\u030Ce\u0309n\u0303t\u0326e\u0308d\u0323", + // áçčẻñțëḍ Latin + "\u016B\u00F1\u0257\u0119\u0211\u0142\u00ED\u00F1\u0119", + // Pseudo-ese ūñɗęȑłíñę + "\u01DDu\u0131\u0283\u0279\u01DDpun", // Upside down ǝuıʃɹǝpun + "\u03C5\u03C0\u03BF\u03B3\u03C1\u03AC\u03BC\u03BC\u03B9\u03C3\u03B7", + // Greek υπογράμμιση + "\u0441\u0438\u043B\u044C\u043D\u0443\u044E", // Russian сильную + "\u0C2C\u0C32\u0C2E\u0C46\u0C56\u0C28", // Telugu బలమైన + "\u508D\u7DDA\u3059\u308B", // Japanese 傍線する + "\uD841\uDF0E\uD841\uDF31\uD841\uDF79\uD843\uDC53\uD843\uDC78", + // Chinese (supplementary plane) + "\uD801\uDC14\uD801\uDC2F\uD801\uDC45\uD801\uDC28\uD801\uDC49\uD801\uDC2F\uD801\uDC3B", + // Deseret 𐐔𐐯𐑅𐐨𐑉𐐯𐐻 + ]; + + const scanTXTstructs = [ + { + delimiter: "/", + tag: "i", + class: "moz-txt-slash", + }, + { + delimiter: "*", + tag: "b", + class: "moz-txt-star", + }, + { + delimiter: "_", + tag: "span", + class: "moz-txt-underscore", + }, + { + delimiter: "|", + tag: "code", + class: "moz-txt-verticalline", + }, + ]; + + const scanHTMLtests = [ + { + input: "http://foo.example.com", + shouldChange: true, + }, + { + input: " <a href='http://a.example.com/'>foo</a>", + shouldChange: false, + }, + { + input: "<abbr>see http://abbr.example.com</abbr>", + shouldChange: true, + }, + { + input: "<!-- see http://comment.example.com/ -->", + shouldChange: false, + }, + { + input: "<!-- greater > -->", + shouldChange: false, + }, + { + input: "<!-- lesser < -->", + shouldChange: false, + }, + { + input: + "<style id='ex'>background-image: url(http://example.com/ex.png);</style>", + shouldChange: false, + }, + { + input: "<style>body > p, body > div { color:blue }</style>", + shouldChange: false, + }, + { + input: "<script>window.location='http://script.example.com/';</script>", + shouldChange: false, + }, + { + input: "<head><title>http://head.example.com/</title></head>", + shouldChange: false, + }, + { + input: "<header>see http://header.example.com</header>", + shouldChange: true, + }, + { + input: "<iframe src='http://iframe.example.com/' />", + shouldChange: false, + }, + { + input: "broken end <script", + shouldChange: false, + }, + ]; + + function hrefLink(url) { + return ' href="' + url + '"'; + } + + function linkText(plaintext) { + return ">" + plaintext + "</a>"; + } + + for (let i = 0; i < scanTXTtests.length; i++) { + let t = scanTXTtests[i]; + let output = converter.scanTXT(t.input, Ci.mozITXTToHTMLConv.kURLs); + let link = hrefLink(t.url); + let text; + if (t.text) { + text = linkText(t.text); + } + if (!output.includes(link)) { + do_throw( + "Unexpected conversion by scanTXT: input=" + + t.input + + ", output=" + + output + + ", link=" + + link + ); + } + if (text && !output.includes(text)) { + do_throw( + "Unexpected conversion by scanTXT: input=" + + t.input + + ", output=" + + output + + ", text=" + + text + ); + } + } + + for (let i = 0; i < scanTXTglyph.length; i++) { + let t = scanTXTglyph[i]; + let output = converter.scanTXT( + t.input, + Ci.mozITXTToHTMLConv.kGlyphSubstitution + ); + for (let j = 0; j < t.results.length; j++) { + if (!output.includes(t.results[j])) { + do_throw( + "Unexpected conversion by scanTXT: input=" + + t.input + + ", output=" + + output + + ", expected=" + + t.results[j] + ); + } + } + } + + for (let i = 0; i < scanTXTstrings.length; ++i) { + for (let j = 0; j < scanTXTstructs.length; ++j) { + let input = + scanTXTstructs[j].delimiter + + scanTXTstrings[i] + + scanTXTstructs[j].delimiter; + let expected = + "<" + + scanTXTstructs[j].tag + + ' class="' + + scanTXTstructs[j].class + + '">' + + '<span class="moz-txt-tag">' + + scanTXTstructs[j].delimiter + + "</span>" + + scanTXTstrings[i] + + '<span class="moz-txt-tag">' + + scanTXTstructs[j].delimiter + + "</span>" + + "</" + + scanTXTstructs[j].tag + + ">"; + let actual = converter.scanTXT(input, Ci.mozITXTToHTMLConv.kStructPhrase); + Assert.equal(encodeURIComponent(actual), encodeURIComponent(expected)); + } + } + + for (let i = 0; i < scanHTMLtests.length; i++) { + let t = scanHTMLtests[i]; + let output = converter.scanHTML(t.input, Ci.mozITXTToHTMLConv.kURLs); + let changed = t.input != output; + if (changed != t.shouldChange) { + do_throw( + "Unexpected change by scanHTML: changed=" + + changed + + ", shouldChange=" + + t.shouldChange + + ", \ninput=" + + t.input + + ", \noutput=" + + output + ); + } + } +} diff --git a/netwerk/test/unit/test_multipart_byteranges.js b/netwerk/test/unit/test_multipart_byteranges.js new file mode 100644 index 0000000000..84ea4249ed --- /dev/null +++ b/netwerk/test/unit/test_multipart_byteranges.js @@ -0,0 +1,141 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; + +XPCOMUtils.defineLazyGetter(this, "uri", function () { + return "http://localhost:" + httpserver.identity.primaryPort + "/multipart"; +}); + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var multipartBody = + "--boundary\r\n" + + "Content-type: text/plain\r\n" + + "Content-range: bytes 0-2/10\r\n" + + "\r\n" + + "aaa\r\n" + + "--boundary\r\n" + + "Content-type: text/plain\r\n" + + "Content-range: bytes 3-7/10\r\n" + + "\r\n" + + "bbbbb" + + "\r\n" + + "--boundary\r\n" + + "Content-type: text/plain\r\n" + + "Content-range: bytes 8-9/10\r\n" + + "\r\n" + + "cc" + + "\r\n" + + "--boundary--"; + +function contentHandler(metadata, response) { + response.setHeader( + "Content-Type", + 'multipart/byteranges; boundary="boundary"' + ); + response.bodyOutputStream.write(multipartBody, multipartBody.length); +} + +var numTests = 2; +var testNum = 0; + +var testData = [ + { + data: "aaa", + type: "text/plain", + isByteRangeRequest: true, + startRange: 0, + endRange: 2, + }, + { + data: "bbbbb", + type: "text/plain", + isByteRangeRequest: true, + startRange: 3, + endRange: 7, + }, + { + data: "cc", + type: "text/plain", + isByteRangeRequest: true, + startRange: 8, + endRange: 9, + }, +]; + +function responseHandler(request, buffer) { + Assert.equal(buffer, testData[testNum].data); + Assert.equal( + request.QueryInterface(Ci.nsIChannel).contentType, + testData[testNum].type + ); + Assert.equal( + request.QueryInterface(Ci.nsIByteRangeRequest).isByteRangeRequest, + testData[testNum].isByteRangeRequest + ); + Assert.equal( + request.QueryInterface(Ci.nsIByteRangeRequest).startRange, + testData[testNum].startRange + ); + Assert.equal( + request.QueryInterface(Ci.nsIByteRangeRequest).endRange, + testData[testNum].endRange + ); + if (++testNum == numTests) { + httpserver.stop(do_test_finished); + } +} + +var multipartListener = { + _buffer: "", + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + this._buffer = ""; + }, + + onDataAvailable(request, stream, offset, count) { + try { + this._buffer = this._buffer.concat(read_stream(stream, count)); + dump("BUFFEEE: " + this._buffer + "\n\n"); + } catch (ex) { + do_throw("Error in onDataAvailable: " + ex); + } + }, + + onStopRequest(request, status) { + try { + responseHandler(request, this._buffer); + } catch (ex) { + do_throw("Error in closure function: " + ex); + } + }, +}; + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/multipart", contentHandler); + httpserver.start(-1); + + var streamConv = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + var conv = streamConv.asyncConvertData( + "multipart/byteranges", + "*/*", + multipartListener, + null + ); + + var chan = make_channel(uri); + chan.asyncOpen(conv, null); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js b/netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js new file mode 100644 index 0000000000..6ee2630746 --- /dev/null +++ b/netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js @@ -0,0 +1,113 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; + +XPCOMUtils.defineLazyGetter(this, "uri", function () { + return "http://localhost:" + httpserver.identity.primaryPort + "/multipart"; +}); + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var multipartBody = + "--boundary\r\n\r\nSome text\r\n--boundary\r\nContent-Type: text/x-test\r\n\r\n<?xml version='1.1'?>\r\n<root/>\r\n--boundary\r\n\r\n<?xml version='1.0'?><root/>\r\n--boundary--"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", 'multipart/mixed; boundary="boundary"'); + response.processAsync(); + + var body = multipartBody; + function byteByByte() { + if (!body.length) { + response.finish(); + return; + } + + var onebyte = body[0]; + response.bodyOutputStream.write(onebyte, 1); + body = body.substring(1); + do_timeout(1, byteByByte); + } + + do_timeout(1, byteByByte); +} + +var numTests = 2; +var testNum = 0; + +var testData = [ + { data: "Some text", type: "text/plain" }, + { data: "<?xml version='1.1'?>\r\n<root/>", type: "text/x-test" }, + { data: "<?xml version='1.0'?><root/>", type: "text/xml" }, +]; + +function responseHandler(request, buffer) { + Assert.equal(buffer, testData[testNum].data); + Assert.equal( + request.QueryInterface(Ci.nsIChannel).contentType, + testData[testNum].type + ); + if (++testNum == numTests) { + httpserver.stop(do_test_finished); + } +} + +var multipartListener = { + _buffer: "", + _index: 0, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + this._buffer = ""; + }, + + onDataAvailable(request, stream, offset, count) { + try { + this._buffer = this._buffer.concat(read_stream(stream, count)); + dump("BUFFEEE: " + this._buffer + "\n\n"); + } catch (ex) { + do_throw("Error in onDataAvailable: " + ex); + } + }, + + onStopRequest(request, status) { + this._index++; + // Second part should be last part + Assert.equal( + request.QueryInterface(Ci.nsIMultiPartChannel).isLastPart, + this._index == testData.length + ); + try { + responseHandler(request, this._buffer); + } catch (ex) { + do_throw("Error in closure function: " + ex); + } + }, +}; + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/multipart", contentHandler); + httpserver.start(-1); + + var streamConv = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + var conv = streamConv.asyncConvertData( + "multipart/mixed", + "*/*", + multipartListener, + null + ); + + var chan = make_channel(uri); + chan.asyncOpen(conv); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_multipart_streamconv.js b/netwerk/test/unit/test_multipart_streamconv.js new file mode 100644 index 0000000000..40b4c4eb3c --- /dev/null +++ b/netwerk/test/unit/test_multipart_streamconv.js @@ -0,0 +1,98 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; + +XPCOMUtils.defineLazyGetter(this, "uri", function () { + return "http://localhost:" + httpserver.identity.primaryPort + "/multipart"; +}); + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var multipartBody = + "--boundary\r\nSet-Cookie: foo=bar\r\n\r\nSome text\r\n--boundary\r\nContent-Type: text/x-test\r\n\r\n<?xml version='1.1'?>\r\n<root/>\r\n--boundary\r\n\r\n<?xml version='1.0'?><root/>\r\n--boundary--"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", 'multipart/mixed; boundary="boundary"'); + response.bodyOutputStream.write(multipartBody, multipartBody.length); +} + +var numTests = 2; +var testNum = 0; + +var testData = [ + { data: "Some text", type: "text/plain" }, + { data: "<?xml version='1.1'?>\r\n<root/>", type: "text/x-test" }, + { data: "<?xml version='1.0'?><root/>", type: "text/xml" }, +]; + +function responseHandler(request, buffer) { + Assert.equal(buffer, testData[testNum].data); + Assert.equal( + request.QueryInterface(Ci.nsIChannel).contentType, + testData[testNum].type + ); + if (++testNum == numTests) { + httpserver.stop(do_test_finished); + } +} + +var multipartListener = { + _buffer: "", + _index: 0, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + this._buffer = ""; + }, + + onDataAvailable(request, stream, offset, count) { + try { + this._buffer = this._buffer.concat(read_stream(stream, count)); + dump("BUFFEEE: " + this._buffer + "\n\n"); + } catch (ex) { + do_throw("Error in onDataAvailable: " + ex); + } + }, + + onStopRequest(request, status) { + this._index++; + // Second part should be last part + Assert.equal( + request.QueryInterface(Ci.nsIMultiPartChannel).isLastPart, + this._index == testData.length + ); + try { + responseHandler(request, this._buffer); + } catch (ex) { + do_throw("Error in closure function: " + ex); + } + }, +}; + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/multipart", contentHandler); + httpserver.start(-1); + + var streamConv = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + var conv = streamConv.asyncConvertData( + "multipart/mixed", + "*/*", + multipartListener, + null + ); + + var chan = make_channel(uri); + chan.asyncOpen(conv); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_multipart_streamconv_empty.js b/netwerk/test/unit/test_multipart_streamconv_empty.js new file mode 100644 index 0000000000..68bc5e6be8 --- /dev/null +++ b/netwerk/test/unit/test_multipart_streamconv_empty.js @@ -0,0 +1,68 @@ +"use strict"; + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +add_task(async function test_empty() { + let uri = "http://localhost"; + let httpChan = make_channel(uri); + + let channel = Cc["@mozilla.org/network/input-stream-channel;1"] + .createInstance(Ci.nsIInputStreamChannel) + .QueryInterface(Ci.nsIChannel); + + channel.setURI(httpChan.URI); + channel.loadInfo = httpChan.loadInfo; + + let inputStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + inputStream.setUTF8Data(""); // Pass an empty string + + channel.contentStream = inputStream; + + let [status, buffer] = await new Promise(resolve => { + let streamConv = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + let multipartListener = { + _buffer: "", + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) {}, + onDataAvailable(request, stream, offset, count) { + try { + this._buffer = this._buffer.concat(read_stream(stream, count)); + dump("BUFFEEE: " + this._buffer + "\n\n"); + } catch (ex) { + do_throw("Error in onDataAvailable: " + ex); + } + }, + + onStopRequest(request, status) { + resolve([status, this._buffer]); + }, + }; + let conv = streamConv.asyncConvertData( + "multipart/mixed", + "*/*", + multipartListener, + null + ); + + let chan = make_channel(uri); + chan.asyncOpen(conv); + }); + + Assert.notEqual( + status, + Cr.NS_OK, + "Should be an error code because content has no boundary" + ); + Assert.equal(buffer, "", "Should have received no content"); +}); diff --git a/netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js b/netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js new file mode 100644 index 0000000000..5d9a04c998 --- /dev/null +++ b/netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js @@ -0,0 +1,90 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; + +XPCOMUtils.defineLazyGetter(this, "uri", function () { + return "http://localhost:" + httpserver.identity.primaryPort + "/multipart"; +}); + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var multipartBody = + "\r\nboundary\r\n\r\nSome text\r\nboundary\r\n\r\n<?xml version='1.0'?><root/>\r\nboundary--"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", 'multipart/mixed; boundary="boundary"'); + response.bodyOutputStream.write(multipartBody, multipartBody.length); +} + +var numTests = 2; +var testNum = 0; + +var testData = [ + { data: "Some text", type: "text/plain" }, + { data: "<?xml version='1.0'?><root/>", type: "text/xml" }, +]; + +function responseHandler(request, buffer) { + Assert.equal(buffer, testData[testNum].data); + Assert.equal( + request.QueryInterface(Ci.nsIChannel).contentType, + testData[testNum].type + ); + if (++testNum == numTests) { + httpserver.stop(do_test_finished); + } +} + +var multipartListener = { + _buffer: "", + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + this._buffer = ""; + }, + + onDataAvailable(request, stream, offset, count) { + try { + this._buffer = this._buffer.concat(read_stream(stream, count)); + dump("BUFFEEE: " + this._buffer + "\n\n"); + } catch (ex) { + do_throw("Error in onDataAvailable: " + ex); + } + }, + + onStopRequest(request, status) { + try { + responseHandler(request, this._buffer); + } catch (ex) { + do_throw("Error in closure function: " + ex); + } + }, +}; + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/multipart", contentHandler); + httpserver.start(-1); + + var streamConv = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + var conv = streamConv.asyncConvertData( + "multipart/mixed", + "*/*", + multipartListener, + null + ); + + var chan = make_channel(uri); + chan.asyncOpen(conv); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js b/netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js new file mode 100644 index 0000000000..e29de88c86 --- /dev/null +++ b/netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js @@ -0,0 +1,90 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; + +XPCOMUtils.defineLazyGetter(this, "uri", function () { + return "http://localhost:" + httpserver.identity.primaryPort + "/multipart"; +}); + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var multipartBody = + "Preamble\r\n--boundary\r\n\r\nSome text\r\n--boundary\r\n\r\n<?xml version='1.0'?><root/>\r\n--boundary--"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", 'multipart/mixed; boundary="boundary"'); + response.bodyOutputStream.write(multipartBody, multipartBody.length); +} + +var numTests = 2; +var testNum = 0; + +var testData = [ + { data: "Some text", type: "text/plain" }, + { data: "<?xml version='1.0'?><root/>", type: "text/xml" }, +]; + +function responseHandler(request, buffer) { + Assert.equal(buffer, testData[testNum].data); + Assert.equal( + request.QueryInterface(Ci.nsIChannel).contentType, + testData[testNum].type + ); + if (++testNum == numTests) { + httpserver.stop(do_test_finished); + } +} + +var multipartListener = { + _buffer: "", + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + this._buffer = ""; + }, + + onDataAvailable(request, stream, offset, count) { + try { + this._buffer = this._buffer.concat(read_stream(stream, count)); + dump("BUFFEEE: " + this._buffer + "\n\n"); + } catch (ex) { + do_throw("Error in onDataAvailable: " + ex); + } + }, + + onStopRequest(request, status) { + try { + responseHandler(request, this._buffer); + } catch (ex) { + do_throw("Error in closure function: " + ex); + } + }, +}; + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/multipart", contentHandler); + httpserver.start(-1); + + var streamConv = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + var conv = streamConv.asyncConvertData( + "multipart/mixed", + "*/*", + multipartListener, + null + ); + + var chan = make_channel(uri); + chan.asyncOpen(conv); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_nestedabout_serialize.js b/netwerk/test/unit/test_nestedabout_serialize.js new file mode 100644 index 0000000000..a8554150d2 --- /dev/null +++ b/netwerk/test/unit/test_nestedabout_serialize.js @@ -0,0 +1,39 @@ +"use strict"; + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +const BinaryOutputStream = Components.Constructor( + "@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream" +); + +const Pipe = Components.Constructor("@mozilla.org/pipe;1", "nsIPipe", "init"); + +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"].createInstance( + Ci.nsIIOService + ); + + var baseURI = ios.newURI("http://example.com/", "UTF-8"); + + // This depends on the redirector for about:license having the + // nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT flag. + var aboutLicense = ios.newURI("about:license", "UTF-8", baseURI); + + var pipe = new Pipe(false, false, 0, 0, null); + var output = new BinaryOutputStream(pipe.outputStream); + var input = new BinaryInputStream(pipe.inputStream); + output.QueryInterface(Ci.nsIObjectOutputStream); + input.QueryInterface(Ci.nsIObjectInputStream); + + output.writeCompoundObject(aboutLicense, Ci.nsIURI, true); + var copy = input.readObject(true); + copy.QueryInterface(Ci.nsIURI); + + Assert.equal(copy.asciiSpec, aboutLicense.asciiSpec); + Assert.ok(copy.equals(aboutLicense)); +} diff --git a/netwerk/test/unit/test_net_addr.js b/netwerk/test/unit/test_net_addr.js new file mode 100644 index 0000000000..331fa9bc74 --- /dev/null +++ b/netwerk/test/unit/test_net_addr.js @@ -0,0 +1,216 @@ +"use strict"; + +var CC = Components.Constructor; + +const ServerSocket = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init" +); + +/** + * TestServer: A single instance of this is created as |serv|. When created, + * it starts listening on the loopback address on port |serv.port|. Tests will + * connect to it after setting |serv.acceptCallback|, which is invoked after it + * accepts a connection. + * + * Within |serv.acceptCallback|, various properties of |serv| can be used to + * run checks. After the callback, the connection is closed, but the server + * remains listening until |serv.stop| + * + * Note: TestServer can only handle a single connection at a time. Tests + * should use run_next_test at the end of |serv.acceptCallback| to start the + * following test which creates a connection. + */ +function TestServer() { + this.reset(); + + // start server. + // any port (-1), loopback only (true), default backlog (-1) + this.listener = ServerSocket(-1, true, -1); + this.port = this.listener.port; + info("server: listening on " + this.port); + this.listener.asyncListen(this); +} + +TestServer.prototype = { + onSocketAccepted(socket, trans) { + info("server: got client connection"); + + // one connection at a time. + if (this.input !== null) { + try { + socket.close(); + } catch (ignore) {} + do_throw("Test written to handle one connection at a time."); + } + + try { + this.input = trans.openInputStream(0, 0, 0); + this.output = trans.openOutputStream(0, 0, 0); + this.selfAddr = trans.getScriptableSelfAddr(); + this.peerAddr = trans.getScriptablePeerAddr(); + + this.acceptCallback(); + } catch (e) { + /* In a native callback such as onSocketAccepted, exceptions might not + * get output correctly or logged to test output. Send them through + * do_throw, which fails the test immediately. */ + do_report_unexpected_exception(e, "in TestServer.onSocketAccepted"); + } + + this.reset(); + }, + + onStopListening(socket) {}, + + /** + * Called to close a connection and clean up properties. + */ + reset() { + if (this.input) { + try { + this.input.close(); + } catch (ignore) {} + } + if (this.output) { + try { + this.output.close(); + } catch (ignore) {} + } + + this.input = null; + this.output = null; + this.acceptCallback = null; + this.selfAddr = null; + this.peerAddr = null; + }, + + /** + * Cleanup for TestServer and this test case. + */ + stop() { + this.reset(); + try { + this.listener.close(); + } catch (ignore) {} + }, +}; + +/** + * Helper function. + * Compares two nsINetAddr objects and ensures they are logically equivalent. + */ +function checkAddrEqual(lhs, rhs) { + Assert.equal(lhs.family, rhs.family); + + if (lhs.family === Ci.nsINetAddr.FAMILY_INET) { + Assert.equal(lhs.address, rhs.address); + Assert.equal(lhs.port, rhs.port); + } + + /* TODO: fully support ipv6 and local */ +} + +/** + * An instance of SocketTransportService, used to create connections. + */ +var sts; + +/** + * Single instance of TestServer + */ +var serv; + +/** + * A place for individual tests to place Objects of importance for access + * throughout asynchronous testing. Particularly important for any output or + * input streams opened, as cleanup of those objects (by the garbage collector) + * causes the stream to close and may have other side effects. + */ +var testDataStore = null; + +/** + * IPv4 test. + */ +function testIpv4() { + testDataStore = { + transport: null, + ouput: null, + }; + + serv.acceptCallback = function () { + // disable the timeoutCallback + serv.timeoutCallback = function () {}; + + var selfAddr = testDataStore.transport.getScriptableSelfAddr(); + var peerAddr = testDataStore.transport.getScriptablePeerAddr(); + + // check peerAddr against expected values + Assert.equal(peerAddr.family, Ci.nsINetAddr.FAMILY_INET); + Assert.equal(peerAddr.port, testDataStore.transport.port); + Assert.equal(peerAddr.port, serv.port); + Assert.equal(peerAddr.address, "127.0.0.1"); + + // check selfAddr against expected values + Assert.equal(selfAddr.family, Ci.nsINetAddr.FAMILY_INET); + Assert.equal(selfAddr.address, "127.0.0.1"); + + // check that selfAddr = server.peerAddr and vice versa. + checkAddrEqual(selfAddr, serv.peerAddr); + checkAddrEqual(peerAddr, serv.selfAddr); + + testDataStore = null; + executeSoon(run_next_test); + }; + + // Useful timeout for debugging test hangs + /*serv.timeoutCallback = function(tname) { + if (tname === 'testIpv4') + do_throw('testIpv4 never completed a connection to TestServ'); + }; + do_timeout(connectTimeout, function(){ serv.timeoutCallback('testIpv4'); });*/ + + testDataStore.transport = sts.createTransport( + [], + "127.0.0.1", + serv.port, + null, + null + ); + /* + * Need to hold |output| so that the output stream doesn't close itself and + * the associated connection. + */ + testDataStore.output = testDataStore.transport.openOutputStream( + Ci.nsITransport.OPEN_BLOCKING, + 0, + 0 + ); + + /* NEXT: + * openOutputStream -> onSocketAccepted -> acceptedCallback -> run_next_test + * OR (if the above timeout is uncommented) + * <connectTimeout lapses> -> timeoutCallback -> do_throw + */ +} + +/** + * Running the tests. + */ +function run_test() { + sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( + Ci.nsISocketTransportService + ); + serv = new TestServer(); + + registerCleanupFunction(function () { + serv.stop(); + }); + + add_test(testIpv4); + /* TODO: testIpv6 */ + /* TODO: testLocal */ + + run_next_test(); +} diff --git a/netwerk/test/unit/test_network_connectivity_service.js b/netwerk/test/unit/test_network_connectivity_service.js new file mode 100644 index 0000000000..c71896cec3 --- /dev/null +++ b/netwerk/test/unit/test_network_connectivity_service.js @@ -0,0 +1,213 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +/** + * Waits for an observer notification to fire. + * + * @param {String} topic The notification topic. + * @returns {Promise} A promise that fulfills when the notification is fired. + */ +function promiseObserverNotification(topic, matchFunc) { + return new Promise((resolve, reject) => { + Services.obs.addObserver(function observe(subject, topic, data) { + let matches = typeof matchFunc != "function" || matchFunc(subject, data); + if (!matches) { + return; + } + Services.obs.removeObserver(observe, topic); + resolve({ subject, data }); + }, topic); + }); +} + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.connectivity-service.DNSv4.domain"); + Services.prefs.clearUserPref("network.connectivity-service.DNSv6.domain"); + Services.prefs.clearUserPref("network.captive-portal-service.testMode"); + Services.prefs.clearUserPref("network.connectivity-service.IPv4.url"); + Services.prefs.clearUserPref("network.connectivity-service.IPv6.url"); +}); + +let httpserver = null; +let httpserverv6 = null; +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort + "/content"; +}); + +XPCOMUtils.defineLazyGetter(this, "URLv6", function () { + return "http://[::1]:" + httpserverv6.identity.primaryPort + "/content"; +}); + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + + const responseBody = "anybody"; + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +const kDNSv6Domain = + mozinfo.os == "linux" || mozinfo.os == "android" + ? "ip6-localhost" + : "localhost"; + +add_task(async function testDNS() { + let ncs = Cc[ + "@mozilla.org/network/network-connectivity-service;1" + ].getService(Ci.nsINetworkConnectivityService); + + // Set the endpoints, trigger a DNS recheck, and wait for it to complete. + Services.prefs.setCharPref( + "network.connectivity-service.DNSv4.domain", + "example.org" + ); + Services.prefs.setCharPref( + "network.connectivity-service.DNSv6.domain", + kDNSv6Domain + ); + ncs.recheckDNS(); + await promiseObserverNotification( + "network:connectivity-service:dns-checks-complete" + ); + + equal( + ncs.DNSv4, + Ci.nsINetworkConnectivityService.OK, + "Check DNSv4 support (expect OK)" + ); + equal( + ncs.DNSv6, + Ci.nsINetworkConnectivityService.OK, + "Check DNSv6 support (expect OK)" + ); + + // Set the endpoints to non-exitant domains, trigger a DNS recheck, and wait for it to complete. + Services.prefs.setCharPref( + "network.connectivity-service.DNSv4.domain", + "does-not-exist.example" + ); + Services.prefs.setCharPref( + "network.connectivity-service.DNSv6.domain", + "does-not-exist.example" + ); + let observerNotification = promiseObserverNotification( + "network:connectivity-service:dns-checks-complete" + ); + ncs.recheckDNS(); + await observerNotification; + + equal( + ncs.DNSv4, + Ci.nsINetworkConnectivityService.NOT_AVAILABLE, + "Check DNSv4 support (expect N/A)" + ); + equal( + ncs.DNSv6, + Ci.nsINetworkConnectivityService.NOT_AVAILABLE, + "Check DNSv6 support (expect N/A)" + ); + + // Set the endpoints back to the proper domains, and simulate a captive portal + // event. + Services.prefs.setCharPref( + "network.connectivity-service.DNSv4.domain", + "example.org" + ); + Services.prefs.setCharPref( + "network.connectivity-service.DNSv6.domain", + kDNSv6Domain + ); + observerNotification = promiseObserverNotification( + "network:connectivity-service:dns-checks-complete" + ); + Services.obs.notifyObservers(null, "network:captive-portal-connectivity"); + // This will cause the state to go to UNKNOWN for a bit, until the check is completed. + equal( + ncs.DNSv4, + Ci.nsINetworkConnectivityService.UNKNOWN, + "Check DNSv4 support (expect UNKNOWN)" + ); + equal( + ncs.DNSv6, + Ci.nsINetworkConnectivityService.UNKNOWN, + "Check DNSv6 support (expect UNKNOWN)" + ); + + await observerNotification; + + equal( + ncs.DNSv4, + Ci.nsINetworkConnectivityService.OK, + "Check DNSv4 support (expect OK)" + ); + equal( + ncs.DNSv6, + Ci.nsINetworkConnectivityService.OK, + "Check DNSv6 support (expect OK)" + ); + + httpserver = new HttpServer(); + httpserver.registerPathHandler("/content", contentHandler); + httpserver.start(-1); + + httpserverv6 = new HttpServer(); + httpserverv6.registerPathHandler("/contentt", contentHandler); + httpserverv6._start(-1, "[::1]"); + + // Before setting the pref, this status is unknown in automation + equal( + ncs.IPv4, + Ci.nsINetworkConnectivityService.UNKNOWN, + "Check IPv4 support (expect UNKNOWN)" + ); + equal( + ncs.IPv6, + Ci.nsINetworkConnectivityService.UNKNOWN, + "Check IPv6 support (expect UNKNOWN)" + ); + + Services.prefs.setBoolPref("network.captive-portal-service.testMode", true); + Services.prefs.setCharPref("network.connectivity-service.IPv4.url", URL); + Services.prefs.setCharPref("network.connectivity-service.IPv6.url", URLv6); + observerNotification = promiseObserverNotification( + "network:connectivity-service:ip-checks-complete" + ); + ncs.recheckIPConnectivity(); + await observerNotification; + + equal( + ncs.IPv4, + Ci.nsINetworkConnectivityService.OK, + "Check IPv4 support (expect OK)" + ); + equal( + ncs.IPv6, + Ci.nsINetworkConnectivityService.OK, + "Check IPv6 support (expect OK)" + ); + + // check that the CPS status is NOT_AVAILABLE when the endpoint is down. + await new Promise(resolve => httpserver.stop(resolve)); + await new Promise(resolve => httpserverv6.stop(resolve)); + observerNotification = promiseObserverNotification( + "network:connectivity-service:ip-checks-complete" + ); + Services.obs.notifyObservers(null, "network:captive-portal-connectivity"); + await observerNotification; + + equal( + ncs.IPv4, + Ci.nsINetworkConnectivityService.NOT_AVAILABLE, + "Check IPv4 support (expect NOT_AVAILABLE)" + ); + equal( + ncs.IPv6, + Ci.nsINetworkConnectivityService.NOT_AVAILABLE, + "Check IPv6 support (expect NOT_AVAILABLE)" + ); +}); diff --git a/netwerk/test/unit/test_networking_over_socket_process.js b/netwerk/test/unit/test_networking_over_socket_process.js new file mode 100644 index 0000000000..e73841a4ee --- /dev/null +++ b/netwerk/test/unit/test_networking_over_socket_process.js @@ -0,0 +1,168 @@ +/* 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"; + +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +let h2Port; +let trrServer; + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +function setup() { + Services.prefs.setIntPref("network.max_socket_process_failed_count", 2); + trr_test_setup(); + + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + Assert.ok(mozinfo.socketprocess_networking); +} + +setup(); +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.max_socket_process_failed_count"); + trr_clear_prefs(); + if (trrServer) { + await trrServer.stop(); + } +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + } + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +add_task(async function setupTRRServer() { + trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + // Only the last record is valid to use. + await trrServer.registerDoHAnswers("test.example.com", "HTTPS", { + answers: [ + { + name: "test.example.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.example.com", + values: [ + { key: "alpn", value: ["h2"] }, + { key: "port", value: h2Port }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("test.example.com", "A", { + answers: [ + { + name: "test.example.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); +}); + +async function doTestSimpleRequest(fromSocketProcess) { + let { inRecord } = await new TRRDNSListener("test.example.com", "127.0.0.1"); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + Assert.equal(inRecord.resolvedInSocketProcess(), fromSocketProcess); + + let chan = makeChan(`https://test.example.com/server-timing`); + let [req] = await channelOpenPromise(chan); + // Test if this request is done by h2. + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.isLoadedBySocketProcess, fromSocketProcess); +} + +// Test if the data is loaded from socket process. +add_task(async function testSimpleRequest() { + await doTestSimpleRequest(true); +}); + +function killSocketProcess(pid) { + const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService( + Ci.nsIProcessToolsService + ); + ProcessTools.kill(pid); +} + +// Test if socket process is restarted. +add_task(async function testSimpleRequestAfterCrash() { + let socketProcessId = Services.io.socketProcessId; + info(`socket process pid is ${socketProcessId}`); + Assert.ok(socketProcessId != 0); + + killSocketProcess(socketProcessId); + + info("wait socket process restart..."); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + + await doTestSimpleRequest(true); +}); + +// Test if data is loaded from parent process. +add_task(async function testTooManyCrashes() { + let socketProcessId = Services.io.socketProcessId; + info(`socket process pid is ${socketProcessId}`); + Assert.ok(socketProcessId != 0); + + let socketProcessCrashed = false; + Services.obs.addObserver(function observe(subject, topic, data) { + Services.obs.removeObserver(observe, topic); + socketProcessCrashed = true; + }, "network:socket-process-crashed"); + + killSocketProcess(socketProcessId); + await TestUtils.waitForCondition(() => socketProcessCrashed); + await doTestSimpleRequest(false); +}); diff --git a/netwerk/test/unit/test_no_cookies_after_last_pb_exit.js b/netwerk/test/unit/test_no_cookies_after_last_pb_exit.js new file mode 100644 index 0000000000..9a0b2fa4dc --- /dev/null +++ b/netwerk/test/unit/test_no_cookies_after_last_pb_exit.js @@ -0,0 +1,133 @@ +"use strict"; + +do_get_profile(); + +// This test checks that active private-browsing HTTP channels, do not save +// cookies after the termination of the private-browsing session. + +// This test consists in following steps: +// - starts a http server +// - no cookies at this point +// - does a beacon request in private-browsing mode +// - after the completion of the request, a cookie should be set (cookie cleanup) +// - does a beacon request in private-browsing mode and dispatch a +// last-pb-context-exit notification +// - after the completion of the request, no cookies should be set + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +let server; + +function setupServer() { + info("Starting the server..."); + + function beaconHandler(metadata, response) { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 204, "No Content"); + response.setHeader("Set-Cookie", "a=b; path=/beacon; sameSite=lax", false); + response.bodyOutputStream.write("", 0); + } + + server = new HttpServer(); + server.registerPathHandler("/beacon", beaconHandler); + server.start(-1); + next(); +} + +function shutdownServer() { + info("Terminating the server..."); + server.stop(next); +} + +function sendRequest(notification) { + info("Sending a request..."); + + var privateLoadContext = Cu.createPrivateLoadContext(); + + var path = + "http://localhost:" + + server.identity.primaryPort + + "/beacon?" + + Math.random(); + + var uri = NetUtil.newURI(path); + var securityFlags = + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL | + Ci.nsILoadInfo.SEC_COOKIES_INCLUDE; + var principal = Services.scriptSecurityManager.createContentPrincipal(uri, { + privateBrowsingId: 1, + }); + + var chan = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags, + contentPolicyType: Ci.nsIContentPolicy.TYPE_BEACON, + }); + + chan.notificationCallbacks = Cu.createPrivateLoadContext(); + + let loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + + loadGroup.notificationCallbacks = Cu.createPrivateLoadContext(); + chan.loadGroup = loadGroup; + + chan.notificationCallbacks = privateLoadContext; + var channelListener = new ChannelListener(next, null, CL_ALLOW_UNKNOWN_CL); + + if (notification) { + info("Sending notification..."); + Services.obs.notifyObservers(null, "last-pb-context-exited"); + } + + chan.asyncOpen(channelListener); +} + +function checkCookies(hasCookie) { + let cm = Services.cookies; + Assert.equal( + cm.cookieExists("localhost", "/beacon", "a", { privateBrowsingId: 1 }), + hasCookie + ); + cm.removeAll(); + next(); +} + +const steps = [ + setupServer, + + // no cookie at startup + () => checkCookies(false), + + // no last-pb-context-exit notification + () => sendRequest(false), + () => checkCookies(true), + + // last-pb-context-exit notification + () => sendRequest(true), + () => checkCookies(false), + + shutdownServer, +]; + +function next() { + if (!steps.length) { + do_test_finished(); + return; + } + + steps.shift()(); +} + +function run_test() { + // We don't want to have CookieJarSettings blocking this test. + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + do_test_pending(); + next(); +} diff --git a/netwerk/test/unit/test_node_execute.js b/netwerk/test/unit/test_node_execute.js new file mode 100644 index 0000000000..3640514a8e --- /dev/null +++ b/netwerk/test/unit/test_node_execute.js @@ -0,0 +1,87 @@ +// This test checks that the interaction between NodeServer.execute defined in +// httpd.js and the node server that we're interacting with defined in +// moz-http2.js is working properly. +/* global my_defined_var */ + +"use strict"; + +add_task(async function test_execute() { + function f() { + return "bla"; + } + let id = await NodeServer.fork(); + equal(await NodeServer.execute(id, `"hello"`), "hello"); + equal(await NodeServer.execute(id, `(() => "hello")()`), "hello"); + equal(await NodeServer.execute(id, `my_defined_var = 1;`), 1); + equal(await NodeServer.execute(id, `(() => my_defined_var)()`), 1); + equal(await NodeServer.execute(id, `my_defined_var`), 1); + + await NodeServer.execute(id, `not_defined_var`) + .then(() => { + ok(false, "should have thrown"); + }) + .catch(e => { + equal(e.message, "ReferenceError: not_defined_var is not defined"); + ok( + e.stack.includes("moz-http2-child.js"), + `stack should be coming from moz-http2-child.js - ${e.stack}` + ); + }); + await NodeServer.execute("definitely_wrong_id", `"hello"`) + .then(() => { + ok(false, "should have thrown"); + }) + .catch(e => { + equal(e.message, "Error: could not find id"); + ok( + e.stack.includes("moz-http2.js"), + `stack should be coming from moz-http2.js - ${e.stack}` + ); + }); + + // Defines f in the context of the node server. + // The implementation of NodeServer.execute prepends `functionName =` to the + // body of the function we pass so it gets attached to the global context + // in the server. + equal(await NodeServer.execute(id, f), undefined); + equal(await NodeServer.execute(id, `f()`), "bla"); + + class myClass { + static doStuff() { + return my_defined_var; + } + } + + equal(await NodeServer.execute(id, myClass), undefined); + equal(await NodeServer.execute(id, `myClass.doStuff()`), 1); + + equal(await NodeServer.kill(id), undefined); + await NodeServer.execute(id, `f()`) + .then(() => ok(false, "should throw")) + .catch(e => equal(e.message, "Error: could not find id")); + id = await NodeServer.fork(); + // Check that a child process dying during a call throws an error. + await NodeServer.execute(id, `process.exit()`) + .then(() => ok(false, "should throw")) + .catch(e => + equal(e.message, "child process exit closing code: 0 signal: null") + ); + + id = await NodeServer.fork(); + equal( + await NodeServer.execute( + id, + `setTimeout(function() { sendBackResponse(undefined) }, 0); 2` + ), + 2 + ); + await new Promise(resolve => do_timeout(10, resolve)); + await NodeServer.kill(id) + .then(() => ok(false, "should throw")) + .catch(e => + equal( + e.message, + `forked process without handler sent: {"error":"","errorStack":""}\n` + ) + ); +}); diff --git a/netwerk/test/unit/test_nojsredir.js b/netwerk/test/unit/test_nojsredir.js new file mode 100644 index 0000000000..6af8f46ebf --- /dev/null +++ b/netwerk/test/unit/test_nojsredir.js @@ -0,0 +1,65 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + { url: "/test/test", datalen: 16 }, + + // Test that the http channel fails and the response body is suppressed + // bug 255119 + { + url: "/test/test", + responseheader: ["Location: javascript:alert()"], + flags: CL_EXPECT_FAILURE, + datalen: 0, + }, +]; + +function setupChannel(url) { + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + url, + loadUsingSystemPrincipal: true, + }); +} + +function startIter() { + var channel = setupChannel(tests[index].url); + channel.asyncOpen( + new ChannelListener(completeIter, channel, tests[index].flags) + ); +} + +function completeIter(request, data, ctx) { + Assert.ok(data.length == tests[index].datalen); + if (++index < tests.length) { + startIter(); + } else { + httpserver.stop(do_test_finished); + } +} + +function run_test() { + httpserver.registerPathHandler("/test/test", handler); + httpserver.start(-1); + + startIter(); + do_test_pending(); +} + +function handler(metadata, response) { + var body = "thequickbrownfox"; + response.setHeader("Content-Type", "text/plain", false); + + var header = tests[index].responseheader; + if (header != undefined) { + for (var i = 0; i < header.length; i++) { + var splitHdr = header[i].split(": "); + response.setHeader(splitHdr[0], splitHdr[1], false); + } + } + + response.setStatusLine(metadata.httpVersion, 302, "Redirected"); + response.bodyOutputStream.write(body, body.length); +} diff --git a/netwerk/test/unit/test_nsIBufferedOutputStream_writeFrom_block.js b/netwerk/test/unit/test_nsIBufferedOutputStream_writeFrom_block.js new file mode 100644 index 0000000000..577b2c6da8 --- /dev/null +++ b/netwerk/test/unit/test_nsIBufferedOutputStream_writeFrom_block.js @@ -0,0 +1,193 @@ +"use strict"; + +var CC = Components.Constructor; + +var Pipe = CC("@mozilla.org/pipe;1", Ci.nsIPipe, "init"); +var BufferedOutputStream = CC( + "@mozilla.org/network/buffered-output-stream;1", + Ci.nsIBufferedOutputStream, + "init" +); +var ScriptableInputStream = CC( + "@mozilla.org/scriptableinputstream;1", + Ci.nsIScriptableInputStream, + "init" +); + +// Verify that pipes behave as we expect. Subsequent tests assume +// pipes behave as demonstrated here. +add_test(function checkWouldBlockPipe() { + // Create a pipe with a one-byte buffer + var pipe = new Pipe(true, true, 1, 1); + + // Writing two bytes should transfer only one byte, and + // return a partial count, not would-block. + Assert.equal(pipe.outputStream.write("xy", 2), 1); + Assert.equal(pipe.inputStream.available(), 1); + + do_check_throws_nsIException( + () => pipe.outputStream.write("y", 1), + "NS_BASE_STREAM_WOULD_BLOCK" + ); + + // Check that nothing was written to the pipe. + Assert.equal(pipe.inputStream.available(), 1); + run_next_test(); +}); + +// A writeFrom to a buffered stream should return +// NS_BASE_STREAM_WOULD_BLOCK if no data was written. +add_test(function writeFromBlocksImmediately() { + // Create a full pipe for our output stream. This will 'would-block' when + // written to. + var outPipe = new Pipe(true, true, 1, 1); + Assert.equal(outPipe.outputStream.write("x", 1), 1); + + // Create a buffered stream, and fill its buffer, so the next write will + // try to flush. + var buffered = new BufferedOutputStream(outPipe.outputStream, 10); + Assert.equal(buffered.write("0123456789", 10), 10); + + // Create a pipe with some data to be our input stream for the writeFrom + // call. + var inPipe = new Pipe(true, true, 1, 1); + Assert.equal(inPipe.outputStream.write("y", 1), 1); + + Assert.equal(inPipe.inputStream.available(), 1); + do_check_throws_nsIException( + () => buffered.writeFrom(inPipe.inputStream, 1), + "NS_BASE_STREAM_WOULD_BLOCK" + ); + + // No data should have been consumed from the pipe. + Assert.equal(inPipe.inputStream.available(), 1); + + run_next_test(); +}); + +// A writeFrom to a buffered stream should return a partial count if any +// data is written, when the last Flush call can only flush a portion of +// the data. +add_test(function writeFromReturnsPartialCountOnPartialFlush() { + // Create a pipe for our output stream. This will accept five bytes, and + // then 'would-block'. + var outPipe = new Pipe(true, true, 5, 1); + + // Create a reference to the pipe's readable end that can be used + // from JavaScript. + var outPipeReadable = new ScriptableInputStream(outPipe.inputStream); + + // Create a buffered stream whose buffer is too large to be flushed + // entirely to the output pipe. + var buffered = new BufferedOutputStream(outPipe.outputStream, 7); + + // Create a pipe to be our input stream for the writeFrom call. + var inPipe = new Pipe(true, true, 15, 1); + + // Write some data to our input pipe, for the rest of the test to consume. + Assert.equal(inPipe.outputStream.write("0123456789abcde", 15), 15); + Assert.equal(inPipe.inputStream.available(), 15); + + // Write from the input pipe to the buffered stream. The buffered stream + // will fill its seven-byte buffer; and then the flush will only succeed + // in writing five bytes to the output pipe. The writeFrom call should + // return the number of bytes it consumed from inputStream. + Assert.equal(buffered.writeFrom(inPipe.inputStream, 11), 7); + Assert.equal(outPipe.inputStream.available(), 5); + Assert.equal(inPipe.inputStream.available(), 8); + + // The partially-successful Flush should have created five bytes of + // available space in the buffered stream's buffer, so we should be able + // to write five bytes to it without blocking. + Assert.equal(buffered.writeFrom(inPipe.inputStream, 5), 5); + Assert.equal(outPipe.inputStream.available(), 5); + Assert.equal(inPipe.inputStream.available(), 3); + + // Attempting to write any more data should would-block. + do_check_throws_nsIException( + () => buffered.writeFrom(inPipe.inputStream, 1), + "NS_BASE_STREAM_WOULD_BLOCK" + ); + + // No data should have been consumed from the pipe. + Assert.equal(inPipe.inputStream.available(), 3); + + // Push the rest of the data through, checking that it all came through. + Assert.equal(outPipeReadable.available(), 5); + Assert.equal(outPipeReadable.read(5), "01234"); + // Flush returns NS_ERROR_FAILURE if it can't transfer the full amount. + do_check_throws_nsIException(() => buffered.flush(), "NS_ERROR_FAILURE"); + Assert.equal(outPipeReadable.available(), 5); + Assert.equal(outPipeReadable.read(5), "56789"); + buffered.flush(); + Assert.equal(outPipeReadable.available(), 2); + Assert.equal(outPipeReadable.read(2), "ab"); + Assert.equal(buffered.writeFrom(inPipe.inputStream, 3), 3); + buffered.flush(); + Assert.equal(outPipeReadable.available(), 3); + Assert.equal(outPipeReadable.read(3), "cde"); + + run_next_test(); +}); + +// A writeFrom to a buffered stream should return a partial count if any +// data is written, when the last Flush call blocks. +add_test(function writeFromReturnsPartialCountOnBlock() { + // Create a pipe for our output stream. This will accept five bytes, and + // then 'would-block'. + var outPipe = new Pipe(true, true, 5, 1); + + // Create a reference to the pipe's readable end that can be used + // from JavaScript. + var outPipeReadable = new ScriptableInputStream(outPipe.inputStream); + + // Create a buffered stream whose buffer is too large to be flushed + // entirely to the output pipe. + var buffered = new BufferedOutputStream(outPipe.outputStream, 7); + + // Create a pipe to be our input stream for the writeFrom call. + var inPipe = new Pipe(true, true, 15, 1); + + // Write some data to our input pipe, for the rest of the test to consume. + Assert.equal(inPipe.outputStream.write("0123456789abcde", 15), 15); + Assert.equal(inPipe.inputStream.available(), 15); + + // Write enough from the input pipe to the buffered stream to fill the + // output pipe's buffer, and then flush it. Nothing should block or fail, + // but the output pipe should now be full. + Assert.equal(buffered.writeFrom(inPipe.inputStream, 5), 5); + buffered.flush(); + Assert.equal(outPipe.inputStream.available(), 5); + Assert.equal(inPipe.inputStream.available(), 10); + + // Now try to write more from the input pipe than the buffered stream's + // buffer can hold. It will attempt to flush, but the output pipe will + // would-block without accepting any data. writeFrom should return the + // correct partial count. + Assert.equal(buffered.writeFrom(inPipe.inputStream, 10), 7); + Assert.equal(outPipe.inputStream.available(), 5); + Assert.equal(inPipe.inputStream.available(), 3); + + // Attempting to write any more data should would-block. + do_check_throws_nsIException( + () => buffered.writeFrom(inPipe.inputStream, 3), + "NS_BASE_STREAM_WOULD_BLOCK" + ); + + // No data should have been consumed from the pipe. + Assert.equal(inPipe.inputStream.available(), 3); + + // Push the rest of the data through, checking that it all came through. + Assert.equal(outPipeReadable.available(), 5); + Assert.equal(outPipeReadable.read(5), "01234"); + // Flush returns NS_ERROR_FAILURE if it can't transfer the full amount. + do_check_throws_nsIException(() => buffered.flush(), "NS_ERROR_FAILURE"); + Assert.equal(outPipeReadable.available(), 5); + Assert.equal(outPipeReadable.read(5), "56789"); + Assert.equal(buffered.writeFrom(inPipe.inputStream, 3), 3); + buffered.flush(); + Assert.equal(outPipeReadable.available(), 5); + Assert.equal(outPipeReadable.read(5), "abcde"); + + run_next_test(); +}); diff --git a/netwerk/test/unit/test_ntlm_authentication.js b/netwerk/test/unit/test_ntlm_authentication.js new file mode 100644 index 0000000000..1c79bbda3f --- /dev/null +++ b/netwerk/test/unit/test_ntlm_authentication.js @@ -0,0 +1,266 @@ +// This file tests authentication prompt callbacks +// TODO NIT use do_check_eq(expected, actual) consistently, not sometimes eq(actual, expected) + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// Turn off the authentication dialog blocking for this test. +var prefs = Services.prefs; +prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "PORT", function () { + return httpserv.identity.primaryPort; +}); + +const FLAG_RETURN_FALSE = 1 << 0; +const FLAG_WRONG_PASSWORD = 1 << 1; +const FLAG_BOGUS_USER = 1 << 2; +// const FLAG_PREVIOUS_FAILED = 1 << 3; +const CROSS_ORIGIN = 1 << 4; +const FLAG_NO_REALM = 1 << 5; +const FLAG_NON_ASCII_USER_PASSWORD = 1 << 6; + +function AuthPrompt1(flags) { + this.flags = flags; +} + +AuthPrompt1.prototype = { + user: "guest", + pass: "guest", + + expectedRealm: "secret", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]), + + prompt: function ap1_prompt(title, text, realm, save, defaultText, result) { + do_throw("unexpected prompt call"); + }, + + promptUsernameAndPassword: function ap1_promptUP( + title, + text, + realm, + savePW, + user, + pw + ) { + if (this.flags & FLAG_NO_REALM) { + // Note that the realm here isn't actually the realm. it's a pw mgr key. + Assert.equal(URL + " (" + this.expectedRealm + ")", realm); + } + if (!(this.flags & CROSS_ORIGIN)) { + if (!text.includes(this.expectedRealm)) { + do_throw("Text must indicate the realm"); + } + } else if (text.includes(this.expectedRealm)) { + do_throw("There should not be realm for cross origin"); + } + if (!text.includes("localhost")) { + do_throw("Text must indicate the hostname"); + } + if (!text.includes(String(PORT))) { + do_throw("Text must indicate the port"); + } + if (text.includes("-1")) { + do_throw("Text must contain negative numbers"); + } + + if (this.flags & FLAG_RETURN_FALSE) { + return false; + } + + if (this.flags & FLAG_BOGUS_USER) { + this.user = "foo\nbar"; + } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { + this.user = "é"; + } + + user.value = this.user; + if (this.flags & FLAG_WRONG_PASSWORD) { + pw.value = this.pass + ".wrong"; + // Now clear the flag to avoid an infinite loop + this.flags &= ~FLAG_WRONG_PASSWORD; + } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { + pw.value = "é"; + } else { + pw.value = this.pass; + } + return true; + }, + + promptPassword: function ap1_promptPW(title, text, realm, save, pwd) { + do_throw("unexpected promptPassword call"); + }, +}; + +function AuthPrompt2(flags) { + this.flags = flags; +} + +AuthPrompt2.prototype = { + user: "guest", + pass: "guest", + + expectedRealm: "secret", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + + promptAuth: function ap2_promptAuth(channel, level, authInfo) { + authInfo.username = this.user; + authInfo.password = this.pass; + return true; + }, + + asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +function Requestor(flags, versions) { + this.flags = flags; + this.versions = versions; +} + +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface: function requestor_gi(iid) { + if (this.versions & 1 && iid.equals(Ci.nsIAuthPrompt)) { + // Allow the prompt to store state by caching it here + if (!this.prompt1) { + this.prompt1 = new AuthPrompt1(this.flags); + } + return this.prompt1; + } + if (this.versions & 2 && iid.equals(Ci.nsIAuthPrompt2)) { + // Allow the prompt to store state by caching it here + if (!this.prompt2) { + this.prompt2 = new AuthPrompt2(this.flags); + } + return this.prompt2; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompt1: null, + prompt2: null, +}; + +function RealmTestRequestor() {} + +RealmTestRequestor.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIAuthPrompt2", + ]), + + getInterface: function realmtest_interface(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + return this; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + promptAuth: function realmtest_checkAuth(channel, level, authInfo) { + Assert.equal(authInfo.realm, '"foo_bar'); + + return false; + }, + + asyncPromptAuth: function realmtest_async(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +function makeChan(url, loadingUrl) { + var principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(loadingUrl), + {} + ); + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); +} + +// /auth/ntlm/simple +function authNtlmSimple(metadata, response) { + if (!metadata.hasHeader("Authorization")) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", "NTLM", false); + return; + } + + let challenge = metadata.getHeader("Authorization"); + if (!challenge.startsWith("NTLM ")) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + return; + } + + let decoded = atob(challenge.substring(5)); + info(decoded); + + if (!decoded.startsWith("NTLMSSP\0")) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + return; + } + + let isNegotiate = decoded.substring(8).startsWith("\x01\x00\x00\x00"); + let isAuthenticate = decoded.substring(8).startsWith("\x03\x00\x00\x00"); + + if (isNegotiate) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader( + "WWW-Authenticate", + "NTLM TlRMTVNTUAACAAAAAAAAAAAoAAABggAAASNFZ4mrze8AAAAAAAAAAAAAAAAAAAAA", + false + ); + return; + } + + if (isAuthenticate) { + let body = "OK"; + response.bodyOutputStream.write(body, body.length); + return; + } + + // Something else went wrong. + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); +} + +let httpserv; +add_task(async function test_ntlm() { + Services.prefs.setBoolPref("network.auth.force-generic-ntlm", true); + Services.prefs.setBoolPref("network.auth.force-generic-ntlm-v1", true); + + httpserv = new HttpServer(); + httpserv.registerPathHandler("/auth/ntlm/simple", authNtlmSimple); + httpserv.start(-1); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.auth.force-generic-ntlm"); + Services.prefs.clearUserPref("network.auth.force-generic-ntlm-v1"); + + await httpserv.stop(); + }); + + var chan = makeChan(URL + "/auth/ntlm/simple", URL); + + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); + let [req, buf] = await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener((req, buf) => resolve([req, buf]), null) + ); + }); + Assert.ok(buf); + Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); +}); diff --git a/netwerk/test/unit/test_ntlm_proxy_and_web_auth.js b/netwerk/test/unit/test_ntlm_proxy_and_web_auth.js new file mode 100644 index 0000000000..0d8b2a327f --- /dev/null +++ b/netwerk/test/unit/test_ntlm_proxy_and_web_auth.js @@ -0,0 +1,360 @@ +// Unit tests for a NTLM authenticated proxy, proxying for a NTLM authenticated +// web server. +// +// Currently the tests do not determine whether the Authentication dialogs have +// been displayed. +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +function AuthPrompt() {} + +AuthPrompt.prototype = { + user: "guest", + pass: "guest", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + + promptAuth: function ap_promptAuth(channel, level, authInfo) { + authInfo.username = this.user; + authInfo.password = this.pass; + + return true; + }, + + asyncPromptAuth: function ap_async(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +function Requestor() {} + +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface: function requestor_gi(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + // Allow the prompt to store state by caching it here + if (!this.prompt) { + this.prompt = new AuthPrompt(); + } + return this.prompt; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompt: null, +}; + +function makeChan(url, loadingUrl) { + var principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(loadingUrl), + {} + ); + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); +} + +function TestListener(resolve) { + this.resolve = resolve; +} +TestListener.prototype.onStartRequest = function (request, context) { + // Need to do the instanceof to allow request.responseStatus + // to be read. + if (!(request instanceof Ci.nsIHttpChannel)) { + dump("Expecting an HTTP channel"); + } + + Assert.equal(expectedResponse, request.responseStatus, "HTTP Status code"); +}; +TestListener.prototype.onStopRequest = function (request, context, status) { + Assert.equal(expectedRequests, requestsMade, "Number of requests made "); + + this.resolve(); +}; +TestListener.prototype.onDataAvaiable = function ( + request, + context, + stream, + offset, + count +) { + read_stream(stream, count); +}; + +// NTLM Messages, for the received type 1 and 3 messages only check that they +// are of the expected type. +const NTLM_TYPE1_PREFIX = "NTLM TlRMTVNTUAABAAAA"; +const NTLM_TYPE2_PREFIX = "NTLM TlRMTVNTUAACAAAA"; +const NTLM_TYPE3_PREFIX = "NTLM TlRMTVNTUAADAAAA"; +const NTLM_PREFIX_LEN = 21; + +const NTLM_CHALLENGE = + NTLM_TYPE2_PREFIX + + "DAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAAR" + + "ABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUg" + + "BWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwB" + + "lAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; + +const PROXY_CHALLENGE = + NTLM_TYPE2_PREFIX + + "DgAOADgAAAAFgooCqLNOPe2aZOAAAAAAAAAAAFAAUABGAAAA" + + "BgEAAAAAAA9HAFcATAAtAE0ATwBaAAIADgBHAFcATAAtAE0A" + + "TwBaAAEADgBHAFcATAAtAE0ATwBaAAQAAgAAAAMAEgBsAG8A" + + "YwBhAGwAaABvAHMAdAAHAAgAOKEwGEZL0gEAAAAA"; + +// Proxy and Web server responses for the happy path scenario. +// i.e. successful proxy auth and successful web server auth +// +function authHandler(metadata, response) { + let authorization; + let authPrefix; + switch (requestsMade) { + case 0: + // Proxy - First request to the Proxy resppond with a 407 to start auth + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", "NTLM", false); + break; + case 1: + // Proxy - Expecting a type 1 negotiate message from the client + authorization = metadata.getHeader("Proxy-Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false); + break; + case 2: + // Proxy - Expecting a type 3 Authenticate message from the client + // Will respond with a 401 to start web server auth sequence + authorization = metadata.getHeader("Proxy-Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message"); + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", "NTLM", false); + break; + case 3: + // Web Server - Expecting a type 1 negotiate message from the client + authorization = metadata.getHeader("Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", NTLM_CHALLENGE, false); + break; + case 4: + // Web Server - Expecting a type 3 Authenticate message from the client + authorization = metadata.getHeader("Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message"); + response.setStatusLine(metadata.httpVersion, 200, "Successful"); + break; + default: + // We should be authenticated and further requests are permitted + authorization = metadata.getHeader("Authorization"); + authorization = metadata.getHeader("Proxy-Authorization"); + Assert.isnull(authorization); + response.setStatusLine(metadata.httpVersion, 200, "Successful"); + } + requestsMade++; +} + +// Proxy responses simulating an invalid proxy password +// Note: that the connection should not be reused after the +// proxy auth fails. +// +function authHandlerInvalidProxyPassword(metadata, response) { + let authorization; + let authPrefix; + switch (requestsMade) { + case 0: + // Proxy - First request respond with a 407 to initiate auth sequence + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", "NTLM", false); + break; + case 1: + // Proxy - Expecting a type 1 negotiate message from the client + authorization = metadata.getHeader("Proxy-Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false); + break; + case 2: + // Proxy - Expecting a type 3 Authenticate message from the client + // Respond with a 407 to indicate invalid credentials + // + authorization = metadata.getHeader("Proxy-Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message"); + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", "NTLM", false); + break; + default: + // Strictly speaking the connection should not be reused at this point + // and reaching here should be an error, but have commented out for now + //dump( "ERROR: NTLM Proxy Authentication, connection should not be reused"); + //Assert.fail(); + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + } + requestsMade++; +} + +// Proxy and web server responses simulating a successful Proxy auth +// and a failed web server auth +// Note: the connection should not be reused once the password failure is +// detected +function authHandlerInvalidWebPassword(metadata, response) { + let authorization; + let authPrefix; + switch (requestsMade) { + case 0: + // Proxy - First request return a 407 to start Proxy auth + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", "NTLM", false); + break; + case 1: + // Proxy - Expecting a type 1 negotiate message from the client + authorization = metadata.getHeader("Proxy-Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", NTLM_CHALLENGE, false); + break; + case 2: + // Proxy - Expecting a type 3 Authenticate message from the client + // Responds with a 401 to start web server auth + authorization = metadata.getHeader("Proxy-Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message"); + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", "NTLM", false); + break; + case 3: + // Web Server - Expecting a type 1 negotiate message from the client + authorization = metadata.getHeader("Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", NTLM_CHALLENGE, false); + break; + case 4: + // Web Server - Expecting a type 3 Authenticate message from the client + // Respond with a 401 to restart the auth sequence. + authorization = metadata.getHeader("Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 1 message"); + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + break; + default: + // We should not get called past step 4 + Assert.ok( + false, + "ERROR: NTLM Auth failed connection should not be reused" + ); + } + requestsMade++; +} + +// Tests to run test_bad_proxy_pass and test_bad_web_pass are split into two stages +// so that once we determine how detect password dialog displays we can check +// that the are displayed correctly, i.e. proxy password should not be prompted +// for when retrying the web server password + +var httpserver = null; +function setup() { + httpserver = new HttpServer(); + httpserver.start(-1); + + Services.prefs.setCharPref("network.proxy.http", "localhost"); + Services.prefs.setIntPref( + "network.proxy.http_port", + httpserver.identity.primaryPort + ); + Services.prefs.setCharPref("network.proxy.no_proxies_on", ""); + Services.prefs.setIntPref("network.proxy.type", 1); + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.proxy.http"); + Services.prefs.clearUserPref("network.proxy.http_port"); + Services.prefs.clearUserPref("network.proxy.no_proxies_on"); + Services.prefs.clearUserPref("network.proxy.type"); + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + + await httpserver.stop(); + }); +} +setup(); + +var expectedRequests = 0; // Number of HTTP requests that are expected +var requestsMade = 0; // The number of requests that were made +var expectedResponse = 0; // The response code +// Note that any test failures in the HTTP handler +// will manifest as a 500 response code + +// Common test setup +// Parameters: +// path - path component of the URL +// handler - http handler function for the httpserver +// requests - expected number oh http requests +// response - expected http response code +// clearCache - clear the authentication cache before running the test +function setupTest(path, handler, requests, response, clearCache) { + requestsMade = 0; + expectedRequests = requests; + expectedResponse = response; + + // clear the auth cache if requested + if (clearCache) { + dump("Clearing auth cache"); + Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager) + .clearAll(); + } + + return new Promise(resolve => { + var chan = makeChan(URL + path, URL); + httpserver.registerPathHandler(path, handler); + chan.notificationCallbacks = new Requestor(); + chan.asyncOpen(new TestListener(resolve)); + }); +} + +// Happy code path +// Succesful proxy and web server auth. +add_task(async function test_happy_path() { + dump("RUNNING TEST: test_happy_path"); + await setupTest("/auth", authHandler, 5, 200, 1); +}); + +// Failed proxy authentication +add_task(async function test_bad_proxy_pass_stage01() { + dump("RUNNING TEST: test_bad_proxy_pass_stage01"); + await setupTest("/auth", authHandlerInvalidProxyPassword, 4, 407, 1); +}); +// Successful logon after failed proxy auth +add_task(async function test_bad_proxy_pass_stage02() { + dump("RUNNING TEST: test_bad_proxy_pass_stage02"); + await setupTest("/auth", authHandler, 5, 200, 0); +}); + +// successful proxy logon, unsuccessful web server sign on +add_task(async function test_bad_web_pass_stage01() { + dump("RUNNING TEST: test_bad_web_pass_stage01"); + await setupTest("/auth", authHandlerInvalidWebPassword, 5, 401, 1); +}); +// successful logon after failed web server auth. +add_task(async function test_bad_web_pass_stage02() { + dump("RUNNING TEST: test_bad_web_pass_stage02"); + await setupTest("/auth", authHandler, 5, 200, 0); +}); diff --git a/netwerk/test/unit/test_ntlm_proxy_auth.js b/netwerk/test/unit/test_ntlm_proxy_auth.js new file mode 100644 index 0000000000..872e969176 --- /dev/null +++ b/netwerk/test/unit/test_ntlm_proxy_auth.js @@ -0,0 +1,361 @@ +// Unit tests for a NTLM authenticated proxy +// +// Currently the tests do not determine whether the Authentication dialogs have +// been displayed. +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +function AuthPrompt() {} + +AuthPrompt.prototype = { + user: "guest", + pass: "guest", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + + promptAuth: function ap_promptAuth(channel, level, authInfo) { + authInfo.username = this.user; + authInfo.password = this.pass; + + return true; + }, + + asyncPromptAuth: function ap_async(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +function Requestor() {} + +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface: function requestor_gi(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + // Allow the prompt to store state by caching it here + if (!this.prompt) { + this.prompt = new AuthPrompt(); + } + return this.prompt; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompt: null, +}; + +function makeChan(url, loadingUrl) { + var principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(loadingUrl), + {} + ); + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); +} + +function TestListener(resolve) { + this.resolve = resolve; +} +TestListener.prototype.onStartRequest = function (request, context) { + // Need to do the instanceof to allow request.responseStatus + // to be read. + if (!(request instanceof Ci.nsIHttpChannel)) { + dump("Expecting an HTTP channel"); + } + + Assert.equal(expectedResponse, request.responseStatus, "HTTP Status code"); +}; +TestListener.prototype.onStopRequest = function (request, context, status) { + Assert.equal(expectedRequests, requestsMade, "Number of requests made "); + Assert.equal( + exptTypeOneCount, + ntlmTypeOneCount, + "Number of type one messages received" + ); + Assert.equal( + exptTypeTwoCount, + ntlmTypeTwoCount, + "Number of type two messages received" + ); + + this.resolve(); +}; +TestListener.prototype.onDataAvaiable = function ( + request, + context, + stream, + offset, + count +) { + read_stream(stream, count); +}; + +// NTLM Messages, for the received type 1 and 3 messages only check that they +// are of the expected type. +const NTLM_TYPE1_PREFIX = "NTLM TlRMTVNTUAABAAAA"; +const NTLM_TYPE2_PREFIX = "NTLM TlRMTVNTUAACAAAA"; +const NTLM_TYPE3_PREFIX = "NTLM TlRMTVNTUAADAAAA"; +const NTLM_PREFIX_LEN = 21; + +const PROXY_CHALLENGE = + NTLM_TYPE2_PREFIX + + "DgAOADgAAAAFgooCqLNOPe2aZOAAAAAAAAAAAFAAUABGAAAA" + + "BgEAAAAAAA9HAFcATAAtAE0ATwBaAAIADgBHAFcATAAtAE0A" + + "TwBaAAEADgBHAFcATAAtAE0ATwBaAAQAAgAAAAMAEgBsAG8A" + + "YwBhAGwAaABvAHMAdAAHAAgAOKEwGEZL0gEAAAAA"; + +// Proxy responses for the happy path scenario. +// i.e. successful proxy auth +// +function successfulAuth(metadata, response) { + let authorization; + let authPrefix; + switch (requestsMade) { + case 0: + // Proxy - First request to the Proxy resppond with a 407 to start auth + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", "NTLM", false); + break; + case 1: + // Proxy - Expecting a type 1 negotiate message from the client + authorization = metadata.getHeader("Proxy-Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false); + break; + case 2: + // Proxy - Expecting a type 3 Authenticate message from the client + // Will respond with a 401 to start web server auth sequence + authorization = metadata.getHeader("Proxy-Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message"); + response.setStatusLine(metadata.httpVersion, 200, "Successful"); + break; + default: + // We should be authenticated and further requests are permitted + authorization = metadata.getHeader("Proxy-Authorization"); + Assert.isnull(authorization); + response.setStatusLine(metadata.httpVersion, 200, "Successful"); + } + requestsMade++; +} + +// Proxy responses simulating an invalid proxy password +// Note: that the connection should not be reused after the +// proxy auth fails. +// +function failedAuth(metadata, response) { + let authorization; + let authPrefix; + switch (requestsMade) { + case 0: + // Proxy - First request respond with a 407 to initiate auth sequence + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", "NTLM", false); + break; + case 1: + // Proxy - Expecting a type 1 negotiate message from the client + authorization = metadata.getHeader("Proxy-Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false); + break; + case 2: + // Proxy - Expecting a type 3 Authenticate message from the client + // Respond with a 407 to indicate invalid credentials + // + authorization = metadata.getHeader("Proxy-Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message"); + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", "NTLM", false); + break; + default: + // Strictly speaking the connection should not be reused at this point + // commenting out for now. + dump("ERROR: NTLM Proxy Authentication, connection should not be reused"); + // assert.fail(); + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + } + requestsMade++; +} +// +// Simulate a connection reset once the connection has been authenticated +// Detects bug 486508 +// +function connectionReset(metadata, response) { + let authorization; + let authPrefix; + switch (requestsMade) { + case 0: + // Proxy - First request to the Proxy resppond with a 407 to start auth + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", "NTLM", false); + break; + case 1: + // Proxy - Expecting a type 1 negotiate message from the client + authorization = metadata.getHeader("Proxy-Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); + ntlmTypeOneCount++; + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false); + break; + case 2: + authorization = metadata.getHeader("Proxy-Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message"); + ntlmTypeTwoCount++; + response.seizePower(); + response.bodyOutPutStream.close(); + response.finish(); + break; + default: + // Should not get any further requests on this channel + dump("ERROR: NTLM Proxy Authentication, connection should not be reused"); + Assert.ok(false); + } + requestsMade++; +} + +// +// Reset the connection after a nogotiate message has been received +// +function connectionReset02(metadata, response) { + switch (requestsMade) { + case 0: + // Proxy - First request to the Proxy resppond with a 407 to start auth + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", "NTLM", false); + break; + case 1: + // Proxy - Expecting a type 1 negotiate message from the client + var authorization = metadata.getHeader("Proxy-Authorization"); + var authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); + ntlmTypeOneCount++; + response.setStatusLine(metadata.httpVersion, 407, "Unauthorized"); + response.setHeader("Proxy-Authenticate", PROXY_CHALLENGE, false); + response.finish(); + response.seizePower(); + response.bodyOutPutStream.close(); + break; + default: + // Should not get any further requests on this channel + dump("ERROR: NTLM Proxy Authentication, connection should not be reused"); + Assert.ok(false); + } + requestsMade++; +} + +var httpserver = null; +function setup() { + httpserver = new HttpServer(); + httpserver.start(-1); + + Services.prefs.setCharPref("network.proxy.http", "localhost"); + Services.prefs.setIntPref( + "network.proxy.http_port", + httpserver.identity.primaryPort + ); + Services.prefs.setCharPref("network.proxy.no_proxies_on", ""); + Services.prefs.setIntPref("network.proxy.type", 1); + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.proxy.http"); + Services.prefs.clearUserPref("network.proxy.http_port"); + Services.prefs.clearUserPref("network.proxy.no_proxies_on"); + Services.prefs.clearUserPref("network.proxy.type"); + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + + await httpserver.stop(); + }); +} +setup(); + +var expectedRequests = 0; // Number of HTTP requests that are expected +var requestsMade = 0; // The number of requests that were made +var expectedResponse = 0; // The response code +// Note that any test failures in the HTTP handler +// will manifest as a 500 response code + +// Common test setup +// Parameters: +// path - path component of the URL +// handler - http handler function for the httpserver +// requests - expected number oh http requests +// response - expected http response code +// clearCache - clear the authentication cache before running the test +function setupTest(path, handler, requests, response, clearCache) { + requestsMade = 0; + expectedRequests = requests; + expectedResponse = response; + + // clear the auth cache if requested + if (clearCache) { + dump("Clearing auth cache"); + Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager) + .clearAll(); + } + + return new Promise(resolve => { + var chan = makeChan(URL + path, URL); + httpserver.registerPathHandler(path, handler); + chan.notificationCallbacks = new Requestor(); + chan.asyncOpen(new TestListener(resolve)); + }); +} + +// Happy code path +// Succesful proxy auth. +add_task(async function test_happy_path() { + dump("RUNNING TEST: test_happy_path"); + await setupTest("/auth", successfulAuth, 3, 200, 1); +}); + +// Failed proxy authentication +add_task(async function test_failed_auth() { + dump("RUNNING TEST:failed auth "); + await setupTest("/auth", failedAuth, 4, 407, 1); +}); + +var ntlmTypeOneCount = 0; // The number of NTLM type one messages received +var exptTypeOneCount = 0; // The number of NTLM type one messages that should be received + +var ntlmTypeTwoCount = 0; // The number of NTLM type two messages received +var exptTypeTwoCount = 0; // The number of NTLM type two messages that should received +// Test connection reset, after successful auth +add_task(async function test_connection_reset() { + dump("RUNNING TEST:connection reset "); + ntlmTypeOneCount = 0; + ntlmTypeTwoCount = 0; + exptTypeOneCount = 1; + exptTypeTwoCount = 1; + await setupTest("/auth", connectionReset, 2, 500, 1); +}); + +// Test connection reset after sending a negotiate. +add_task(async function test_connection_reset02() { + dump("RUNNING TEST:connection reset "); + ntlmTypeOneCount = 0; + ntlmTypeTwoCount = 0; + exptTypeOneCount = 1; + exptTypeTwoCount = 0; + await setupTest("/auth", connectionReset02, 1, 500, 1); +}); diff --git a/netwerk/test/unit/test_ntlm_web_auth.js b/netwerk/test/unit/test_ntlm_web_auth.js new file mode 100644 index 0000000000..a4493e2504 --- /dev/null +++ b/netwerk/test/unit/test_ntlm_web_auth.js @@ -0,0 +1,249 @@ +// Unit tests for a NTLM authenticated web server. +// +// Currently the tests do not determine whether the Authentication dialogs have +// been displayed. +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +function AuthPrompt() {} + +AuthPrompt.prototype = { + user: "guest", + pass: "guest", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + + promptAuth: function ap_promptAuth(channel, level, authInfo) { + authInfo.username = this.user; + authInfo.password = this.pass; + + return true; + }, + + asyncPromptAuth: function ap_async(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +function Requestor() {} + +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface: function requestor_gi(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + // Allow the prompt to store state by caching it here + if (!this.prompt) { + this.prompt = new AuthPrompt(); + } + return this.prompt; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompt: null, +}; + +function makeChan(url, loadingUrl) { + var principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(loadingUrl), + {} + ); + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); +} + +function TestListener() {} +TestListener.prototype.onStartRequest = function (request, context) { + // Need to do the instanceof to allow request.responseStatus + // to be read. + if (!(request instanceof Ci.nsIHttpChannel)) { + dump("Expecting an HTTP channel"); + } + + Assert.equal(expectedResponse, request.responseStatus, "HTTP Status code"); +}; +TestListener.prototype.onStopRequest = function (request, context, status) { + Assert.equal(expectedRequests, requestsMade, "Number of requests made "); + + if (current_test < tests.length - 1) { + current_test++; + tests[current_test](); + } else { + do_test_pending(); + httpserver.stop(do_test_finished); + } + + do_test_finished(); +}; +TestListener.prototype.onDataAvaiable = function ( + request, + context, + stream, + offset, + count +) { + read_stream(stream, count); +}; + +// NTLM Messages, for the received type 1 and 3 messages only check that they +// are of the expected type. +const NTLM_TYPE1_PREFIX = "NTLM TlRMTVNTUAABAAAA"; +const NTLM_TYPE2_PREFIX = "NTLM TlRMTVNTUAACAAAA"; +const NTLM_TYPE3_PREFIX = "NTLM TlRMTVNTUAADAAAA"; +const NTLM_PREFIX_LEN = 21; + +const NTLM_CHALLENGE = + NTLM_TYPE2_PREFIX + + "DAAMADAAAAABAoEAASNFZ4mrze8AAAAAAAAAAGIAYgA8AAAAR" + + "ABPAE0AQQBJAE4AAgAMAEQATwBNAEEASQBOAAEADABTAEUAUg" + + "BWAEUAUgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIAcwB" + + "lAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMAbwBtAAAAAAA="; + +// Web server responses for the happy path scenario. +// i.e. successful web server auth +// +function successfulAuth(metadata, response) { + let authorization; + let authPrefix; + switch (requestsMade) { + case 0: + // Web Server - Initial request + // Will respond with a 401 to start web server auth sequence + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", "NTLM", false); + break; + case 1: + // Web Server - Expecting a type 1 negotiate message from the client + authorization = metadata.getHeader("Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", NTLM_CHALLENGE, false); + break; + case 2: + // Web Server - Expecting a type 3 Authenticate message from the client + authorization = metadata.getHeader("Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 3 message"); + response.setStatusLine(metadata.httpVersion, 200, "Successful"); + break; + default: + // We should be authenticated and further requests are permitted + authorization = metadata.getHeader("Authorization"); + Assert.isnull(authorization); + response.setStatusLine(metadata.httpVersion, 200, "Successful"); + } + requestsMade++; +} + +// web server responses simulating an unsuccessful web server auth +function failedAuth(metadata, response) { + let authorization; + let authPrefix; + switch (requestsMade) { + case 0: + // Web Server - First request return a 401 to start auth sequence + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", "NTLM", false); + break; + case 1: + // Web Server - Expecting a type 1 negotiate message from the client + authorization = metadata.getHeader("Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE1_PREFIX, authPrefix, "Expecting a Type 1 message"); + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", NTLM_CHALLENGE, false); + break; + case 2: + // Web Server - Expecting a type 3 Authenticate message from the client + // Respond with a 401 to restart the auth sequence. + authorization = metadata.getHeader("Authorization"); + authPrefix = authorization.substring(0, NTLM_PREFIX_LEN); + Assert.equal(NTLM_TYPE3_PREFIX, authPrefix, "Expecting a Type 1 message"); + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + break; + default: + // We should not get called past step 2 + // Strictly speaking the connection should not be used again + // commented out for testing + // dump( "ERROR: NTLM Auth failed connection should not be reused"); + //Assert.fail(); + response.setHeader("WWW-Authenticate", "NTLM", false); + } + requestsMade++; +} + +var tests = [test_happy_path, test_failed_auth]; +var current_test = 0; + +var httpserver = null; +function run_test() { + httpserver = new HttpServer(); + httpserver.start(-1); + + tests[0](); +} + +var expectedRequests = 0; // Number of HTTP requests that are expected +var requestsMade = 0; // The number of requests that were made +var expectedResponse = 0; // The response code +// Note that any test failures in the HTTP handler +// will manifest as a 500 response code + +// Common test setup +// Parameters: +// path - path component of the URL +// handler - http handler function for the httpserver +// requests - expected number oh http requests +// response - expected http response code +// clearCache - clear the authentication cache before running the test +function setupTest(path, handler, requests, response, clearCache) { + requestsMade = 0; + expectedRequests = requests; + expectedResponse = response; + + // clear the auth cache if requested + if (clearCache) { + dump("Clearing auth cache"); + Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager) + .clearAll(); + } + + var chan = makeChan(URL + path, URL); + httpserver.registerPathHandler(path, handler); + chan.notificationCallbacks = new Requestor(); + chan.asyncOpen(new TestListener()); + + return chan; +} + +// Happy code path +// Succesful web server auth. +function test_happy_path() { + dump("RUNNING TEST: test_happy_path"); + setupTest("/auth", successfulAuth, 3, 200, 1); + + do_test_pending(); +} + +// Unsuccessful web server sign on +function test_failed_auth() { + dump("RUNNING TEST: test_failed_auth"); + setupTest("/auth", failedAuth, 3, 401, 1); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_oblivious_http.js b/netwerk/test/unit/test_oblivious_http.js new file mode 100644 index 0000000000..95199a53e9 --- /dev/null +++ b/netwerk/test/unit/test_oblivious_http.js @@ -0,0 +1,176 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +class ObliviousHttpTestRequest { + constructor(method, uri, headers, content) { + this.method = method; + this.uri = uri; + this.headers = headers; + this.content = content; + } +} + +class ObliviousHttpTestResponse { + constructor(status, headers, content) { + this.status = status; + this.headers = headers; + this.content = content; + } +} + +class ObliviousHttpTestCase { + constructor(request, response) { + this.request = request; + this.response = response; + } +} + +add_task(async function test_oblivious_http() { + let testcases = [ + new ObliviousHttpTestCase( + new ObliviousHttpTestRequest( + "GET", + NetUtil.newURI("https://example.com"), + { "X-Some-Header": "header value" }, + "" + ), + new ObliviousHttpTestResponse(200, {}, "Hello, World!") + ), + new ObliviousHttpTestCase( + new ObliviousHttpTestRequest( + "POST", + NetUtil.newURI("http://example.test"), + { "X-Some-Header": "header value", "X-Some-Other-Header": "25" }, + "Posting some content..." + ), + new ObliviousHttpTestResponse( + 418, + { "X-Teapot": "teapot" }, + "I'm a teapot" + ) + ), + ]; + + for (let testcase of testcases) { + await run_one_testcase(testcase); + } +}); + +async function run_one_testcase(testcase) { + let ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService( + Ci.nsIObliviousHttp + ); + let ohttpServer = ohttp.server(); + + let httpServer = new HttpServer(); + httpServer.registerPathHandler("/", function (request, response) { + let inputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + inputStream.init(request.bodyInputStream); + let requestBody = inputStream.readBytes(inputStream.available()); + let ohttpResponse = ohttpServer.decapsulate(stringToBytes(requestBody)); + let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService( + Ci.nsIBinaryHttp + ); + let decodedRequest = bhttp.decodeRequest(ohttpResponse.request); + equal(decodedRequest.method, testcase.request.method); + equal(decodedRequest.scheme, testcase.request.uri.scheme); + equal(decodedRequest.authority, testcase.request.uri.hostPort); + equal(decodedRequest.path, testcase.request.uri.pathQueryRef); + for ( + let i = 0; + i < decodedRequest.headerNames.length && + i < decodedRequest.headerValues.length; + i++ + ) { + equal( + decodedRequest.headerValues[i], + testcase.request.headers[decodedRequest.headerNames[i]] + ); + } + equal(bytesToString(decodedRequest.content), testcase.request.content); + + let responseHeaderNames = ["content-type"]; + let responseHeaderValues = ["text/plain"]; + for (let headerName of Object.keys(testcase.response.headers)) { + responseHeaderNames.push(headerName); + responseHeaderValues.push(testcase.response.headers[headerName]); + } + let binaryResponse = new BinaryHttpResponse( + testcase.response.status, + responseHeaderNames, + responseHeaderValues, + stringToBytes(testcase.response.content) + ); + let responseBytes = bhttp.encodeResponse(binaryResponse); + let encResponse = ohttpResponse.encapsulate(responseBytes); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "message/ohttp-res", false); + response.write(bytesToString(encResponse)); + }); + httpServer.start(-1); + + let ohttpService = Cc[ + "@mozilla.org/network/oblivious-http-service;1" + ].getService(Ci.nsIObliviousHttpService); + let relayURI = NetUtil.newURI( + `http://localhost:${httpServer.identity.primaryPort}/` + ); + let obliviousHttpChannel = ohttpService + .newChannel(relayURI, testcase.request.uri, ohttpServer.encodedConfig) + .QueryInterface(Ci.nsIHttpChannel); + for (let headerName of Object.keys(testcase.request.headers)) { + obliviousHttpChannel.setRequestHeader( + headerName, + testcase.request.headers[headerName], + false + ); + } + if (testcase.request.method == "POST") { + let uploadChannel = obliviousHttpChannel.QueryInterface( + Ci.nsIUploadChannel2 + ); + ok(uploadChannel); + let bodyStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + bodyStream.setData( + testcase.request.content, + testcase.request.content.length + ); + uploadChannel.explicitSetUploadStream( + bodyStream, + null, + -1, + testcase.request.method, + false + ); + } + let response = await new Promise((resolve, reject) => { + NetUtil.asyncFetch(obliviousHttpChannel, function (inputStream, result) { + let scriptableInputStream = Cc[ + "@mozilla.org/scriptableinputstream;1" + ].createInstance(Ci.nsIScriptableInputStream); + scriptableInputStream.init(inputStream); + let responseBody = scriptableInputStream.readBytes( + inputStream.available() + ); + resolve(responseBody); + }); + }); + equal(response, testcase.response.content); + for (let headerName of Object.keys(testcase.response.headers)) { + equal( + obliviousHttpChannel.getResponseHeader(headerName), + testcase.response.headers[headerName] + ); + } + await new Promise((resolve, reject) => { + httpServer.stop(resolve); + }); +} diff --git a/netwerk/test/unit/test_obs-fold.js b/netwerk/test/unit/test_obs-fold.js new file mode 100644 index 0000000000..84915aa748 --- /dev/null +++ b/netwerk/test/unit/test_obs-fold.js @@ -0,0 +1,73 @@ +/* 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 { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +let body = "abcd"; +function request_handler1(metadata, response) { + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("X-header-first: FIRSTVALUE\r\n"); + response.write("X-header-second: 1; second\r\n"); + response.write(`Content-Length: ${body.length}\r\n`); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +// This handler is for obs-fold +// The line that contains X-header-second starts with a space. As a consequence +// it gets folded into the previous line. +function request_handler2(metadata, response) { + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("X-header-first: FIRSTVALUE\r\n"); + // Note the space at the begining of the line + response.write(" X-header-second: 1; second\r\n"); + response.write(`Content-Length: ${body.length}\r\n`); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +add_task(async function test() { + let http_server = new HttpServer(); + http_server.registerPathHandler("/test1", request_handler1); + http_server.registerPathHandler("/test2", request_handler2); + http_server.start(-1); + const port = http_server.identity.primaryPort; + + let chan1 = makeChan(`http://localhost:${port}/test1`); + await new Promise(resolve => { + chan1.asyncOpen(new ChannelListener(resolve)); + }); + equal(chan1.getResponseHeader("X-header-first"), "FIRSTVALUE"); + equal(chan1.getResponseHeader("X-header-second"), "1; second"); + + let chan2 = makeChan(`http://localhost:${port}/test2`); + await new Promise(resolve => { + chan2.asyncOpen(new ChannelListener(resolve)); + }); + equal( + chan2.getResponseHeader("X-header-first"), + "FIRSTVALUE X-header-second: 1; second" + ); + Assert.throws( + () => chan2.getResponseHeader("X-header-second"), + /NS_ERROR_NOT_AVAILABLE/ + ); + + await new Promise(resolve => http_server.stop(resolve)); +}); diff --git a/netwerk/test/unit/test_offline_status.js b/netwerk/test/unit/test_offline_status.js new file mode 100644 index 0000000000..22fde0bd20 --- /dev/null +++ b/netwerk/test/unit/test_offline_status.js @@ -0,0 +1,15 @@ +"use strict"; + +function run_test() { + try { + var linkService = Cc[ + "@mozilla.org/network/network-link-service;1" + ].getService(Ci.nsINetworkLinkService); + + // The offline status should depends on the link status + Assert.notEqual(Services.io.offline, linkService.isLinkUp); + } catch (e) { + // The network link service might not be available + Assert.equal(Services.io.offline, false); + } +} diff --git a/netwerk/test/unit/test_ohttp.js b/netwerk/test/unit/test_ohttp.js new file mode 100644 index 0000000000..b7bd2f1d06 --- /dev/null +++ b/netwerk/test/unit/test_ohttp.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function test_known_config() { + let ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService( + Ci.nsIObliviousHttp + ); + let encodedConfig = hexStringToBytes( + "0100209403aafe76dfd4568481e04e44b42d744287eae4070b50e48baa7a91a4e80d5600080001000100010003" + ); + let request = hexStringToBytes( + "00034745540568747470730b6578616d706c652e636f6d012f" + ); + let ohttpRequest = ohttp.encapsulateRequest(encodedConfig, request); + ok(ohttpRequest); +} + +function test_with_server() { + let ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService( + Ci.nsIObliviousHttp + ); + let server = ohttp.server(); + ok(server.encodedConfig); + let request = hexStringToBytes( + "00034745540568747470730b6578616d706c652e636f6d012f" + ); + let ohttpRequest = ohttp.encapsulateRequest(server.encodedConfig, request); + let ohttpResponse = server.decapsulate(ohttpRequest.encRequest); + ok(ohttpResponse); + deepEqual(ohttpResponse.request, request); + let response = hexStringToBytes("0140c8"); + let encResponse = ohttpResponse.encapsulate(response); + deepEqual(ohttpRequest.response.decapsulate(encResponse), response); +} + +function run_test() { + test_known_config(); + test_with_server(); +} diff --git a/netwerk/test/unit/test_orb_empty_header.js b/netwerk/test/unit/test_orb_empty_header.js new file mode 100644 index 0000000000..24866b7073 --- /dev/null +++ b/netwerk/test/unit/test_orb_empty_header.js @@ -0,0 +1,83 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ + +function makeChan(uri) { + var principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "http://example.com" + ); + let chan = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +async function setup() { + if (!inChildProcess()) { + Services.prefs.setBoolPref("browser.opaqueResponseBlocking", true); + } + let server = new NodeHTTPServer(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + await server.registerPathHandler("/dosniff", (req, resp) => { + resp.writeHead(500, { + "Content-Type": "application/json", + "Set-Cookie": "mycookie", + }); + resp.write("good"); + resp.end("done"); + }); + await server.registerPathHandler("/nosniff", (req, resp) => { + resp.writeHead(500, { + "Content-Type": "application/msword", + "Set-Cookie": "mycookie", + }); + resp.write("good"); + resp.end("done"); + }); + + return server; +} +async function test_empty_header(server, doSniff) { + let chan; + if (doSniff) { + chan = makeChan(`${server.origin()}/dosniff`); + } else { + chan = makeChan(`${server.origin()}/nosniff`); + } + let req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)); + }); + equal(req.status, Cr.NS_ERROR_FAILURE); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 500); + + req.visitResponseHeaders({ + visitHeader: function visit(_aName, _aValue) { + ok(false); + }, + }); +} + +add_task(async function () { + let server = await setup(); + await test_empty_header(server, true); + await test_empty_header(server, false); +}); diff --git a/netwerk/test/unit/test_origin.js b/netwerk/test/unit/test_origin.js new file mode 100644 index 0000000000..9ef86e8885 --- /dev/null +++ b/netwerk/test/unit/test_origin.js @@ -0,0 +1,323 @@ +"use strict"; + +var h2Port; +var prefs; +var http2pref; +var extpref; +var loadGroup; + +function run_test() { + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Services.prefs; + + http2pref = prefs.getBoolPref("network.http.http2.enabled"); + extpref = prefs.getBoolPref("network.http.originextension"); + + prefs.setBoolPref("network.http.http2.enabled", true); + prefs.setBoolPref("network.http.originextension", true); + prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, alt1.example.com" + ); + + // The moz-http2 cert is for {foo, alt1, alt2}.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + doTest1(); +} + +function resetPrefs() { + prefs.setBoolPref("network.http.http2.enabled", http2pref); + prefs.setBoolPref("network.http.originextension", extpref); + prefs.clearUserPref("network.dns.localDomains"); +} + +function makeChan(origin) { + return NetUtil.newChannel({ + uri: origin, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +let origin; +var nextTest; +var nextPortExpectedToBeSame = false; +var currentPort = 0; +var forceReload = false; +var forceFailListener = false; + +var Listener = function () {}; +Listener.prototype.clientPort = 0; +Listener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code! (" + request.status + ")"); + } + Assert.equal(request.responseStatus, 200); + this.clientPort = parseInt(request.getResponseHeader("x-client-port")); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.ok(Components.isSuccessCode(status)); + if (nextPortExpectedToBeSame) { + Assert.equal(currentPort, this.clientPort); + } else { + Assert.notEqual(currentPort, this.clientPort); + } + currentPort = this.clientPort; + nextTest(); + do_test_finished(); + }, +}; + +var FailListener = function () {}; +FailListener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.ok(!Components.isSuccessCode(request.status)); + }, + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + onStopRequest: function testOnStopRequest(request, status) { + Assert.ok(!Components.isSuccessCode(request.status)); + nextTest(); + do_test_finished(); + }, +}; + +function testsDone() { + dump("testsDone\n"); + resetPrefs(); +} + +function doTest() { + dump("execute doTest " + origin + "\n"); + var chan = makeChan(origin); + var listener; + if (!forceFailListener) { + listener = new Listener(); + } else { + listener = new FailListener(); + } + forceFailListener = false; + + if (!forceReload) { + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + } else { + chan.loadFlags = + Ci.nsIRequest.LOAD_FRESH_CONNECTION | + Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + } + forceReload = false; + chan.asyncOpen(listener); +} + +function doTest1() { + dump("doTest1()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-1"; + nextTest = doTest2; + nextPortExpectedToBeSame = false; + do_test_pending(); + doTest(); +} + +function doTest2() { + // plain connection reuse + dump("doTest2()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-2"; + nextTest = doTest3; + nextPortExpectedToBeSame = true; + do_test_pending(); + doTest(); +} + +function doTest3() { + // 7540 style coalescing + dump("doTest3()\n"); + origin = "https://alt1.example.com:" + h2Port + "/origin-3"; + nextTest = doTest4; + nextPortExpectedToBeSame = true; + do_test_pending(); + doTest(); +} + +function doTest4() { + // forces an empty origin frame to be omitted + dump("doTest4()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-4"; + nextTest = doTest5; + nextPortExpectedToBeSame = true; + do_test_pending(); + doTest(); +} + +function doTest5() { + // 7540 style coalescing should not work due to empty origin set + dump("doTest5()\n"); + origin = "https://alt1.example.com:" + h2Port + "/origin-5"; + nextTest = doTest6; + nextPortExpectedToBeSame = false; + do_test_pending(); + doTest(); +} + +function doTest6() { + // get a fresh connection with alt1 and alt2 in origin set + // note that there is no dns for alt2 + dump("doTest6()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-6"; + nextTest = doTest7; + nextPortExpectedToBeSame = false; + forceReload = true; + do_test_pending(); + doTest(); +} + +function doTest7() { + // check conn reuse to ensure sni is implicit in origin set + dump("doTest7()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-7"; + nextTest = doTest8; + nextPortExpectedToBeSame = true; + do_test_pending(); + doTest(); +} + +function doTest8() { + // alt1 is in origin set (and is 7540 eligible) + dump("doTest8()\n"); + origin = "https://alt1.example.com:" + h2Port + "/origin-8"; + nextTest = doTest9; + nextPortExpectedToBeSame = true; + do_test_pending(); + doTest(); +} + +function doTest9() { + // alt2 is in origin set but does not have dns + dump("doTest9()\n"); + origin = "https://alt2.example.com:" + h2Port + "/origin-9"; + nextTest = doTest10; + nextPortExpectedToBeSame = true; + do_test_pending(); + doTest(); +} + +function doTest10() { + // bar is in origin set but does not have dns like alt2 + // but the cert is not valid for bar. so expect a failure + dump("doTest10()\n"); + origin = "https://bar.example.com:" + h2Port + "/origin-10"; + nextTest = doTest11; + nextPortExpectedToBeSame = false; + forceFailListener = true; + do_test_pending(); + doTest(); +} + +var Http2PushApiListener = function () {}; + +Http2PushApiListener.prototype = { + fooOK: false, + alt1OK: false, + + getInterface(aIID) { + return this.QueryInterface(aIID); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIHttpPushListener", + "nsIStreamListener", + ]), + + // nsIHttpPushListener + onPush: function onPush(associatedChannel, pushChannel) { + dump( + "push api onpush " + + pushChannel.originalURI.spec + + " associated to " + + associatedChannel.originalURI.spec + + "\n" + ); + + Assert.equal( + associatedChannel.originalURI.spec, + "https://foo.example.com:" + h2Port + "/origin-11-a" + ); + Assert.equal(pushChannel.getRequestHeader("x-pushed-request"), "true"); + + if ( + pushChannel.originalURI.spec === + "https://foo.example.com:" + h2Port + "/origin-11-b" + ) { + this.fooOK = true; + } else if ( + pushChannel.originalURI.spec === + "https://alt1.example.com:" + h2Port + "/origin-11-e" + ) { + this.alt1OK = true; + } else { + // any push of bar or madeup should not end up in onPush() + Assert.equal(true, false); + } + pushChannel.cancel(Cr.NS_ERROR_ABORT); + }, + + // normal Channel listeners + onStartRequest: function pushAPIOnStart(request) { + dump("push api onstart " + request.originalURI.spec + "\n"); + }, + + onDataAvailable: function pushAPIOnDataAvailable( + request, + stream, + offset, + cnt + ) { + read_stream(stream, cnt); + }, + + onStopRequest: function test_onStopR(request, status) { + dump("push api onstop " + request.originalURI.spec + "\n"); + Assert.ok(this.fooOK); + Assert.ok(this.alt1OK); + nextTest(); + do_test_finished(); + }, +}; + +function doTest11() { + // we are connected with an SNI of foo from test6 + // but the origin set is alt1, alt2, bar - foo is implied + // and bar is not actually covered by the cert + // + // the server will push foo (b-OK), bar (c-NOT OK), madeup (d-NOT OK), alt1 (e-OK), + + dump("doTest11()\n"); + do_test_pending(); + loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + var chan = makeChan("https://foo.example.com:" + h2Port + "/origin-11-a"); + chan.loadGroup = loadGroup; + var listener = new Http2PushApiListener(); + nextTest = testsDone; + chan.notificationCallbacks = listener; + chan.asyncOpen(listener); +} diff --git a/netwerk/test/unit/test_original_sent_received_head.js b/netwerk/test/unit/test_original_sent_received_head.js new file mode 100644 index 0000000000..651e8d1458 --- /dev/null +++ b/netwerk/test/unit/test_original_sent_received_head.js @@ -0,0 +1,247 @@ +// +// HTTP headers test +// Response headers can be changed after they have been received, e.g. empty +// headers are deleted, some duplicate header are merged (if no error is +// thrown), etc. +// +// The "original header" is introduced to hold the header array in the order +// and the form as they have been received from the network. +// Here, the "original headers" are tested. +// +// Original headers will be stored in the cache as well. This test checks +// that too. + +// Note: sets Cc and Ci variables + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var testpath = "/simple"; +var httpbody = "0123456789"; + +var dbg = 1; + +function run_test() { + if (dbg) { + print("============== START =========="); + } + + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + run_next_test(); +} + +add_test(function test_headerChange() { + if (dbg) { + print("============== test_headerChange setup: in"); + } + + var channel1 = setupChannel(testpath); + channel1.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + + // ChannelListener defined in head_channels.js + channel1.asyncOpen(new ChannelListener(checkResponse, null)); + + if (dbg) { + print("============== test_headerChange setup: out"); + } +}); + +add_test(function test_fromCache() { + if (dbg) { + print("============== test_fromCache setup: in"); + } + + var channel2 = setupChannel(testpath); + channel2.loadFlags = Ci.nsIRequest.LOAD_FROM_CACHE; + + // ChannelListener defined in head_channels.js + channel2.asyncOpen(new ChannelListener(checkResponse, null)); + + if (dbg) { + print("============== test_fromCache setup: out"); + } +}); + +add_test(function finish() { + if (dbg) { + print("============== STOP =========="); + } + httpserver.stop(do_test_finished); +}); + +function setupChannel(path) { + var chan = NetUtil.newChannel({ + uri: URL + path, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + return chan; +} + +function serverHandler(metadata, response) { + if (dbg) { + print("============== serverHandler: in"); + } + + let etag; + try { + etag = metadata.getHeader("If-None-Match"); + } catch (ex) { + etag = ""; + } + if (etag == "testtag") { + if (dbg) { + print("============== 304 answerr: in"); + } + response.setStatusLine("1.1", 304, "Not Modified"); + } else { + response.setHeader("Content-Type", "text/plain", false); + response.setStatusLine("1.1", 200, "OK"); + + // Set a empty header. A empty link header will not appear in header list, + // but in the "original headers", it will be still exactly as received. + response.setHeaderNoCheck("Link", "", true); + response.setHeaderNoCheck("Link", "value1"); + response.setHeaderNoCheck("Link", "value2"); + response.setHeaderNoCheck("Location", "loc"); + response.setHeader("Cache-Control", "max-age=10000", false); + response.setHeader("ETag", "testtag", false); + response.bodyOutputStream.write(httpbody, httpbody.length); + } + if (dbg) { + print("============== serverHandler: out"); + } +} + +function checkResponse(request, data, context) { + if (dbg) { + print("============== checkResponse: in"); + } + + request.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(request.responseStatus, 200); + Assert.equal(request.responseStatusText, "OK"); + Assert.ok(request.requestSucceeded); + + // Response header have only one link header. + var linkHeaderFound = 0; + var locationHeaderFound = 0; + request.visitResponseHeaders({ + visitHeader: function visit(aName, aValue) { + if (aName == "link") { + linkHeaderFound++; + Assert.equal(aValue, "value1, value2"); + } + if (aName == "location") { + locationHeaderFound++; + Assert.equal(aValue, "loc"); + } + }, + }); + Assert.equal(linkHeaderFound, 1); + Assert.equal(locationHeaderFound, 1); + + // The "original header" still contains 3 link headers. + var linkOrgHeaderFound = 0; + var locationOrgHeaderFound = 0; + request.visitOriginalResponseHeaders({ + visitHeader: function visitOrg(aName, aValue) { + if (aName == "link") { + if (linkOrgHeaderFound == 0) { + Assert.equal(aValue, ""); + } else if (linkOrgHeaderFound == 1) { + Assert.equal(aValue, "value1"); + } else { + Assert.equal(aValue, "value2"); + } + linkOrgHeaderFound++; + } + if (aName == "location") { + locationOrgHeaderFound++; + Assert.equal(aValue, "loc"); + } + }, + }); + Assert.equal(linkOrgHeaderFound, 3); + Assert.equal(locationOrgHeaderFound, 1); + + if (dbg) { + print("============== Remove headers"); + } + // Remove header. + request.setResponseHeader("Link", "", false); + request.setResponseHeader("Location", "", false); + + var linkHeaderFound2 = false; + var locationHeaderFound2 = 0; + request.visitResponseHeaders({ + visitHeader: function visit(aName, aValue) { + if (aName == "Link") { + linkHeaderFound2 = true; + } + if (aName == "Location") { + locationHeaderFound2 = true; + } + }, + }); + Assert.ok(!linkHeaderFound2, "There should be no link header"); + Assert.ok(!locationHeaderFound2, "There should be no location headers."); + + // The "original header" still contains the empty header. + var linkOrgHeaderFound2 = 0; + var locationOrgHeaderFound2 = 0; + request.visitOriginalResponseHeaders({ + visitHeader: function visitOrg(aName, aValue) { + if (aName == "link") { + if (linkOrgHeaderFound2 == 0) { + Assert.equal(aValue, ""); + } else if (linkOrgHeaderFound2 == 1) { + Assert.equal(aValue, "value1"); + } else { + Assert.equal(aValue, "value2"); + } + linkOrgHeaderFound2++; + } + if (aName == "location") { + locationOrgHeaderFound2++; + Assert.equal(aValue, "loc"); + } + }, + }); + Assert.ok(linkOrgHeaderFound2 == 3, "Original link header still here."); + Assert.ok( + locationOrgHeaderFound2 == 1, + "Original location header still here." + ); + + if (dbg) { + print("============== Test GetResponseHeader"); + } + var linkOrgHeaderFound3 = 0; + request.getOriginalResponseHeader("link", { + visitHeader: function visitOrg(aName, aValue) { + if (linkOrgHeaderFound3 == 0) { + Assert.equal(aValue, ""); + } else if (linkOrgHeaderFound3 == 1) { + Assert.equal(aValue, "value1"); + } else { + Assert.equal(aValue, "value2"); + } + linkOrgHeaderFound3++; + }, + }); + Assert.ok(linkOrgHeaderFound2 == 3, "Original link header still here."); + + if (dbg) { + print("============== checkResponse: out"); + } + + run_next_test(); +} diff --git a/netwerk/test/unit/test_pac_reload_after_network_change.js b/netwerk/test/unit/test_pac_reload_after_network_change.js new file mode 100644 index 0000000000..54fa5f3472 --- /dev/null +++ b/netwerk/test/unit/test_pac_reload_after_network_change.js @@ -0,0 +1,73 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +XPCOMUtils.defineLazyServiceGetter( + this, + "gProxyService", + "@mozilla.org/network/protocol-proxy-service;1", + "nsIProtocolProxyService" +); +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +let pacServer; +const proxyPort = 4433; + +add_setup(async function () { + pacServer = new HttpServer(); + pacServer.registerPathHandler( + "/proxy.pac", + function handler(metadata, response) { + let content = `function FindProxyForURL(url, host) { return "HTTPS localhost:${proxyPort}"; }`; + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + } + ); + pacServer.start(-1); +}); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.proxy.type"); + Services.prefs.clearUserPref("network.proxy.autoconfig_url"); + Services.prefs.clearUserPref("network.proxy.reload_pac_delay"); +}); + +async function getProxyInfo() { + return new Promise((resolve, reject) => { + let uri = Services.io.newURI("http://www.mozilla.org/"); + gProxyService.asyncResolve(uri, 0, { + onProxyAvailable(_req, _uri, pi, _status) { + resolve(pi); + }, + }); + }); +} + +// Test if we can successfully get PAC when the PAC server is available. +add_task(async function testPAC() { + // Configure PAC + Services.prefs.setIntPref("network.proxy.type", 2); + Services.prefs.setCharPref( + "network.proxy.autoconfig_url", + `http://localhost:${pacServer.identity.primaryPort}/proxy.pac` + ); + + let pi = await getProxyInfo(); + Assert.equal(pi.port, proxyPort, "Expected proxy port to be the same"); + Assert.equal(pi.type, "https", "Expected proxy type to be https"); +}); + +// When PAC server is down, we should not use proxy at all. +add_task(async function testWhenPACServerDown() { + Services.prefs.setIntPref("network.proxy.reload_pac_delay", 0); + await new Promise(resolve => pacServer.stop(resolve)); + + Services.obs.notifyObservers(null, "network:link-status-changed", "changed"); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 3000)); + + let pi = await getProxyInfo(); + Assert.equal(pi, null, "should have no proxy"); +}); diff --git a/netwerk/test/unit/test_parse_content_type.js b/netwerk/test/unit/test_parse_content_type.js new file mode 100644 index 0000000000..20b76d558f --- /dev/null +++ b/netwerk/test/unit/test_parse_content_type.js @@ -0,0 +1,365 @@ +/* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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"; + +var charset = {}; +var hadCharset = {}; +var type; + +function reset() { + delete charset.value; + delete hadCharset.value; + type = undefined; +} + +function check(aType, aCharset, aHadCharset) { + Assert.equal(type, aType); + Assert.equal(aCharset, charset.value); + Assert.equal(aHadCharset, hadCharset.value); + reset(); +} + +add_task(function test_parseResponseContentType() { + var netutil = Services.io; + + type = netutil.parseRequestContentType("text/html", charset, hadCharset); + check("text/html", "", false); + + type = netutil.parseResponseContentType("text/html", charset, hadCharset); + check("text/html", "", false); + + type = netutil.parseRequestContentType("TEXT/HTML", charset, hadCharset); + check("text/html", "", false); + + type = netutil.parseResponseContentType("TEXT/HTML", charset, hadCharset); + check("text/html", "", false); + + type = netutil.parseRequestContentType( + "text/html, text/html", + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + "text/html, text/html", + charset, + hadCharset + ); + check("text/html", "", false); + + type = netutil.parseRequestContentType( + "text/html, text/plain", + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + "text/html, text/plain", + charset, + hadCharset + ); + check("text/plain", "", false); + + type = netutil.parseRequestContentType("text/html, ", charset, hadCharset); + check("", "", false); + + type = netutil.parseResponseContentType("text/html, ", charset, hadCharset); + check("text/html", "", false); + + type = netutil.parseRequestContentType("text/html, */*", charset, hadCharset); + check("", "", false); + + type = netutil.parseResponseContentType( + "text/html, */*", + charset, + hadCharset + ); + check("text/html", "", false); + + type = netutil.parseRequestContentType("text/html, foo", charset, hadCharset); + check("", "", false); + + type = netutil.parseResponseContentType( + "text/html, foo", + charset, + hadCharset + ); + check("text/html", "", false); + + type = netutil.parseRequestContentType( + "text/html; charset=ISO-8859-1", + charset, + hadCharset + ); + check("text/html", "ISO-8859-1", true); + + type = netutil.parseResponseContentType( + "text/html; charset=ISO-8859-1", + charset, + hadCharset + ); + check("text/html", "ISO-8859-1", true); + + type = netutil.parseRequestContentType( + 'text/html; charset="ISO-8859-1"', + charset, + hadCharset + ); + check("text/html", "ISO-8859-1", true); + + type = netutil.parseResponseContentType( + 'text/html; charset="ISO-8859-1"', + charset, + hadCharset + ); + check("text/html", "ISO-8859-1", true); + + type = netutil.parseRequestContentType( + "text/html; charset='ISO-8859-1'", + charset, + hadCharset + ); + check("text/html", "'ISO-8859-1'", true); + + type = netutil.parseResponseContentType( + "text/html; charset='ISO-8859-1'", + charset, + hadCharset + ); + check("text/html", "'ISO-8859-1'", true); + + type = netutil.parseRequestContentType( + 'text/html; charset="ISO-8859-1", text/html', + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + 'text/html; charset="ISO-8859-1", text/html', + charset, + hadCharset + ); + check("text/html", "ISO-8859-1", true); + + type = netutil.parseRequestContentType( + 'text/html; charset="ISO-8859-1", text/html; charset=UTF8', + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + 'text/html; charset="ISO-8859-1", text/html; charset=UTF8', + charset, + hadCharset + ); + check("text/html", "UTF8", true); + + type = netutil.parseRequestContentType( + "text/html; charset=ISO-8859-1, TEXT/HTML", + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + "text/html; charset=ISO-8859-1, TEXT/HTML", + charset, + hadCharset + ); + check("text/html", "ISO-8859-1", true); + + type = netutil.parseRequestContentType( + "text/html; charset=ISO-8859-1, TEXT/plain", + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + "text/html; charset=ISO-8859-1, TEXT/plain", + charset, + hadCharset + ); + check("text/plain", "", true); + + type = netutil.parseRequestContentType( + "text/plain, TEXT/HTML; charset=ISO-8859-1, text/html, TEXT/HTML", + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + "text/plain, TEXT/HTML; charset=ISO-8859-1, text/html, TEXT/HTML", + charset, + hadCharset + ); + check("text/html", "ISO-8859-1", true); + + type = netutil.parseRequestContentType( + 'text/plain, TEXT/HTML; param="charset=UTF8"; charset="ISO-8859-1"; param2="charset=UTF16", text/html, TEXT/HTML', + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + 'text/plain, TEXT/HTML; param="charset=UTF8"; charset="ISO-8859-1"; param2="charset=UTF16", text/html, TEXT/HTML', + charset, + hadCharset + ); + check("text/html", "ISO-8859-1", true); + + type = netutil.parseRequestContentType( + 'text/plain, TEXT/HTML; param=charset=UTF8; charset="ISO-8859-1"; param2=charset=UTF16, text/html, TEXT/HTML', + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + 'text/plain, TEXT/HTML; param=charset=UTF8; charset="ISO-8859-1"; param2=charset=UTF16, text/html, TEXT/HTML', + charset, + hadCharset + ); + check("text/html", "ISO-8859-1", true); + + type = netutil.parseRequestContentType( + 'text/plain, TEXT/HTML; param=charset=UTF8; charset="ISO-8859-1"; param2=charset=UTF16, text/html, TEXT/HTML', + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseRequestContentType( + "text/plain; param= , text/html", + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + "text/plain; param= , text/html", + charset, + hadCharset + ); + check("text/html", "", false); + + type = netutil.parseRequestContentType( + 'text/plain; param=", text/html"', + charset, + hadCharset + ); + check("text/plain", "", false); + + type = netutil.parseResponseContentType( + 'text/plain; param=", text/html"', + charset, + hadCharset + ); + check("text/plain", "", false); + + type = netutil.parseRequestContentType( + 'text/plain; param=", \\" , text/html"', + charset, + hadCharset + ); + check("text/plain", "", false); + + type = netutil.parseResponseContentType( + 'text/plain; param=", \\" , text/html"', + charset, + hadCharset + ); + check("text/plain", "", false); + + type = netutil.parseRequestContentType( + 'text/plain; param=", \\" , text/html , "', + charset, + hadCharset + ); + check("text/plain", "", false); + + type = netutil.parseResponseContentType( + 'text/plain; param=", \\" , text/html , "', + charset, + hadCharset + ); + check("text/plain", "", false); + + type = netutil.parseRequestContentType( + 'text/plain param=", \\" , text/html , "', + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + 'text/plain param=", \\" , text/html , "', + charset, + hadCharset + ); + check("text/plain", "", false); + + type = netutil.parseRequestContentType( + "text/plain charset=UTF8", + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + "text/plain charset=UTF8", + charset, + hadCharset + ); + check("text/plain", "", false); + + type = netutil.parseRequestContentType( + 'text/plain, TEXT/HTML; param="charset=UTF8"; ; param2="charset=UTF16", text/html, TEXT/HTML', + charset, + hadCharset + ); + check("", "", false); + + type = netutil.parseResponseContentType( + 'text/plain, TEXT/HTML; param="charset=UTF8"; ; param2="charset=UTF16", text/html, TEXT/HTML', + charset, + hadCharset + ); + check("text/html", "", false); + + // Bug 562915 - correctness: "\x" is "x" + type = netutil.parseResponseContentType( + 'text/plain; charset="UTF\\-8"', + charset, + hadCharset + ); + check("text/plain", "UTF-8", true); + + // Bug 700589 + + // check that single quote doesn't confuse parsing of subsequent parameters + type = netutil.parseResponseContentType( + 'text/plain; x=\'; charset="UTF-8"', + charset, + hadCharset + ); + check("text/plain", "UTF-8", true); + + // check that single quotes do not get removed from extracted charset + type = netutil.parseResponseContentType( + "text/plain; charset='UTF-8'", + charset, + hadCharset + ); + check("text/plain", "'UTF-8'", true); +}); diff --git a/netwerk/test/unit/test_partial_response_entry_size_smart_shrink.js b/netwerk/test/unit/test_partial_response_entry_size_smart_shrink.js new file mode 100644 index 0000000000..c9774ef7b0 --- /dev/null +++ b/netwerk/test/unit/test_partial_response_entry_size_smart_shrink.js @@ -0,0 +1,104 @@ +/* + + This is only a crash test. We load a partial content, cache it. Then we change the limit + for single cache entry size (shrink it) so that the next request for the rest of the content + will hit that limit and doom/remove the entry. We change the size manually, but in reality + it's being changed by cache smart size. + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +// Have 2kb response (8 * 2 ^ 8) +var responseBody = "response"; +for (var i = 0; i < 8; ++i) { + responseBody += responseBody; +} + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "range"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Cache-Control", "max-age=360000"); + + if (!metadata.hasHeader("If-Range")) { + response.setHeader("Content-Length", responseBody.length + ""); + response.processAsync(); + let slice = responseBody.slice(0, 100); + response.bodyOutputStream.write(slice, slice.length); + response.finish(); + } else { + let slice = responseBody.slice(100); + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader( + "Content-Range", + (responseBody.length - slice.length).toString() + + "-" + + (responseBody.length - 1).toString() + + "/" + + responseBody.length.toString() + ); + + response.setHeader("Content-Length", slice.length + ""); + response.bodyOutputStream.write(slice, slice.length); + } +} + +let enforceSoftPref; +let enforceStrictChunkedPref; + +function run_test() { + enforceSoftPref = Services.prefs.getBoolPref( + "network.http.enforce-framing.soft" + ); + Services.prefs.setBoolPref("network.http.enforce-framing.soft", false); + + enforceStrictChunkedPref = Services.prefs.getBoolPref( + "network.http.enforce-framing.strict_chunked_encoding" + ); + Services.prefs.setBoolPref( + "network.http.enforce-framing.strict_chunked_encoding", + false + ); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan = make_channel(URL + "/content"); + chan.asyncOpen(new ChannelListener(firstTimeThrough, null, CL_IGNORE_CL)); + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + // Change single cache entry limit to 1 kb. This emulates smart size change. + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + + var chan = make_channel(URL + "/content"); + chan.asyncOpen(new ChannelListener(finish_test, null)); +} + +function finish_test(request, buffer) { + Assert.equal(buffer, responseBody); + Services.prefs.setBoolPref( + "network.http.enforce-framing.soft", + enforceSoftPref + ); + Services.prefs.setBoolPref( + "network.http.enforce-framing.strict_chunked_encoding", + enforceStrictChunkedPref + ); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_permmgr.js b/netwerk/test/unit/test_permmgr.js new file mode 100644 index 0000000000..5d10429d58 --- /dev/null +++ b/netwerk/test/unit/test_permmgr.js @@ -0,0 +1,125 @@ +// tests nsIPermissionManager + +"use strict"; + +var hosts = [ + // format: [host, type, permission] + ["http://mozilla.org", "cookie", 1], + ["http://mozilla.org", "image", 2], + ["http://mozilla.org", "popup", 3], + ["http://mozilla.com", "cookie", 1], + ["http://www.mozilla.com", "cookie", 2], + ["http://dev.mozilla.com", "cookie", 3], +]; + +var results = [ + // format: [host, type, testPermission result, testExactPermission result] + // test defaults + ["http://localhost", "cookie", 0, 0], + ["http://spreadfirefox.com", "cookie", 0, 0], + // test different types + ["http://mozilla.org", "cookie", 1, 1], + ["http://mozilla.org", "image", 2, 2], + ["http://mozilla.org", "popup", 3, 3], + // test subdomains + ["http://www.mozilla.org", "cookie", 1, 0], + ["http://www.dev.mozilla.org", "cookie", 1, 0], + // test different permissions on subdomains + ["http://mozilla.com", "cookie", 1, 1], + ["http://www.mozilla.com", "cookie", 2, 2], + ["http://dev.mozilla.com", "cookie", 3, 3], + ["http://www.dev.mozilla.com", "cookie", 3, 0], +]; + +function run_test() { + Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); + var pm = Services.perms; + + var ioService = Services.io; + + var secMan = Services.scriptSecurityManager; + + // nsIPermissionManager implementation is an extension; don't fail if it's not there + if (!pm) { + return; + } + + // put a few hosts in + for (let i = 0; i < hosts.length; ++i) { + let uri = ioService.newURI(hosts[i][0]); + let principal = secMan.createContentPrincipal(uri, {}); + + pm.addFromPrincipal(principal, hosts[i][1], hosts[i][2]); + } + + // test the result + for (let i = 0; i < results.length; ++i) { + let uri = ioService.newURI(results[i][0]); + let principal = secMan.createContentPrincipal(uri, {}); + + Assert.equal( + pm.testPermissionFromPrincipal(principal, results[i][1]), + results[i][2] + ); + Assert.equal( + pm.testExactPermissionFromPrincipal(principal, results[i][1]), + results[i][3] + ); + } + + // test the all property ... + var perms = pm.all; + Assert.equal(perms.length, hosts.length); + + // ... remove all the hosts ... + for (let j = 0; j < perms.length; ++j) { + pm.removePermission(perms[j]); + } + + // ... ensure each and every element is equal ... + for (let i = 0; i < hosts.length; ++i) { + for (let j = 0; j < perms.length; ++j) { + if ( + perms[j].matchesURI(ioService.newURI(hosts[i][0]), true) && + hosts[i][1] == perms[j].type && + hosts[i][2] == perms[j].capability + ) { + perms.splice(j, 1); + break; + } + } + } + Assert.equal(perms.length, 0); + + // ... and check the permmgr's empty + Assert.equal(pm.all.length, 0); + + // test UTF8 normalization behavior: expect ASCII/ACE host encodings + var utf8 = "b\u00FCcher.dolske.org"; // "bücher.dolske.org" + var aceref = "xn--bcher-kva.dolske.org"; + var principal = secMan.createContentPrincipal( + ioService.newURI("http://" + utf8), + {} + ); + pm.addFromPrincipal(principal, "utf8", 1); + Assert.notEqual(Services.perms.all.length, 0); + var ace = Services.perms.all[0]; + Assert.equal(ace.principal.asciiHost, aceref); + Assert.equal(Services.perms.all.length > 1, false); + + // test removeAll() + pm.removeAll(); + Assert.equal(Services.perms.all.length, 0); + + principal = secMan.createContentPrincipalFromOrigin( + "https://www.example.com" + ); + pm.addFromPrincipal(principal, "offline-app", pm.ALLOW_ACTION); + // Remove existing entry. + let perm = pm.getPermissionObject(principal, "offline-app", true); + pm.removePermission(perm); + // Try to remove already deleted entry. + perm = pm.getPermissionObject(principal, "offline-app", true); + pm.removePermission(perm); + Assert.equal(Services.perms.all.length, 0); +} diff --git a/netwerk/test/unit/test_ping_aboutnetworking.js b/netwerk/test/unit/test_ping_aboutnetworking.js new file mode 100644 index 0000000000..fbaaeaa405 --- /dev/null +++ b/netwerk/test/unit/test_ping_aboutnetworking.js @@ -0,0 +1,103 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 gDashboard = Cc["@mozilla.org/network/dashboard;1"].getService( + Ci.nsIDashboard +); + +function connectionFailed(status) { + let status_ok = [ + "NS_NET_STATUS_RESOLVING_HOST", + "NS_NET_STATUS_RESOLVED_HOST", + "NS_NET_STATUS_CONNECTING_TO", + "NS_NET_STATUS_CONNECTED_TO", + ]; + for (let i = 0; i < status_ok.length; i++) { + if (status == status_ok[i]) { + return false; + } + } + + return true; +} + +function test_sockets(serverSocket) { + // TODO: enable this test in bug 1581892. + if (mozinfo.socketprocess_networking) { + info("skip test_sockets"); + do_test_finished(); + return; + } + + do_test_pending(); + gDashboard.requestSockets(function (data) { + let index = -1; + info("requestSockets: " + JSON.stringify(data.sockets)); + for (let i = 0; i < data.sockets.length; i++) { + if (data.sockets[i].host == "127.0.0.1") { + index = i; + break; + } + } + Assert.notEqual(index, -1); + Assert.equal(data.sockets[index].port, serverSocket.port); + Assert.equal(data.sockets[index].type, "TCP"); + + do_test_finished(); + }); +} + +function run_test() { + var ps = Services.prefs; + // disable network changed events to avoid the the risk of having the dns + // cache getting flushed behind our back + ps.setBoolPref("network.notify.changed", false); + // Localhost is hardcoded to loopback and isn't cached, disable that with this pref + ps.setBoolPref("network.proxy.allow_hijacking_localhost", true); + + registerCleanupFunction(function () { + ps.clearUserPref("network.notify.changed"); + ps.clearUserPref("network.proxy.allow_hijacking_localhost"); + }); + + let serverSocket = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + serverSocket.init(-1, true, -1); + + do_test_pending(); + gDashboard.requestConnection( + "localhost", + serverSocket.port, + "tcp", + 15, + function (connInfo) { + if (connInfo.status == "NS_NET_STATUS_CONNECTED_TO") { + do_test_pending(); + gDashboard.requestDNSInfo(function (data) { + let found = false; + info("requestDNSInfo: " + JSON.stringify(data.entries)); + for (let i = 0; i < data.entries.length; i++) { + if (data.entries[i].hostname == "localhost") { + found = true; + break; + } + } + Assert.equal(found, true); + + do_test_finished(); + test_sockets(serverSocket); + }); + + do_test_finished(); + } + if (connectionFailed(connInfo.status)) { + do_throw(connInfo.status); + } + } + ); +} diff --git a/netwerk/test/unit/test_plaintext_sniff.js b/netwerk/test/unit/test_plaintext_sniff.js new file mode 100644 index 0000000000..fb9620e04c --- /dev/null +++ b/netwerk/test/unit/test_plaintext_sniff.js @@ -0,0 +1,209 @@ +// Test the plaintext-or-binary sniffer + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// List of Content-Type headers to test. For each header we have an array. +// The first element in the array is the Content-Type header string. The +// second element in the array is a boolean indicating whether we allow +// sniffing for that type. +var contentTypeHeaderList = [ + ["text/plain", true], + ["text/plain; charset=ISO-8859-1", true], + ["text/plain; charset=iso-8859-1", true], + ["text/plain; charset=UTF-8", true], + ["text/plain; charset=unknown", false], + ["text/plain; param", false], + ["text/plain; charset=ISO-8859-1; param", false], + ["text/plain; charset=iso-8859-1; param", false], + ["text/plain; charset=UTF-8; param", false], + ["text/plain; charset=utf-8", false], + ["text/plain; charset=utf8", false], + ["text/plain; charset=UTF8", false], + ["text/plain; charset=iSo-8859-1", false], +]; + +// List of response bodies to test. For each response we have an array. The +// first element in the array is the body string. The second element in the +// array is a boolean indicating whether that string should sniff as binary. +var bodyList = [["Plaintext", false]]; + +// List of possible BOMs +var BOMList = [ + "\xFE\xFF", // UTF-16BE + "\xFF\xFE", // UTF-16LE + "\xEF\xBB\xBF", // UTF-8 + "\x00\x00\xFE\xFF", // UCS-4BE + "\x00\x00\xFF\xFE", // UCS-4LE +]; + +// Build up bodyList. The things we treat as binary are ASCII codes 0-8, +// 14-26, 28-31. That is, the control char range, except for tab, newline, +// vertical tab, form feed, carriage return, and ESC (this last being used by +// Shift_JIS, apparently). +function isBinaryChar(ch) { + return ( + (0 <= ch && ch <= 8) || (14 <= ch && ch <= 26) || (28 <= ch && ch <= 31) + ); +} + +// Test chars on their own +var i; +for (i = 0; i <= 127; ++i) { + bodyList.push([String.fromCharCode(i), isBinaryChar(i)]); +} + +// Test that having a BOM prevents plaintext sniffing +var j; +for (i = 0; i <= 127; ++i) { + for (j = 0; j < BOMList.length; ++j) { + bodyList.push([BOMList[j] + String.fromCharCode(i, i), false]); + } +} + +// Test that having a BOM requires at least 4 chars to kick in +for (i = 0; i <= 127; ++i) { + for (j = 0; j < BOMList.length; ++j) { + bodyList.push([ + BOMList[j] + String.fromCharCode(i), + BOMList[j].length == 2 && isBinaryChar(i), + ]); + } +} + +function makeChan(headerIdx, bodyIdx) { + var chan = NetUtil.newChannel({ + uri: + "http://localhost:" + + httpserv.identity.primaryPort + + "/" + + headerIdx + + "/" + + bodyIdx, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + + chan.loadFlags |= Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS; + + return chan; +} + +function makeListener(headerIdx, bodyIdx) { + var listener = { + onStartRequest: function test_onStartR(request) { + try { + var chan = request.QueryInterface(Ci.nsIChannel); + + Assert.equal(chan.status, Cr.NS_OK); + + var type = chan.contentType; + + var expectedType = + contentTypeHeaderList[headerIdx][1] && bodyList[bodyIdx][1] + ? "application/x-vnd.mozilla.guess-from-ext" + : "text/plain"; + if (expectedType != type) { + do_throw( + "Unexpected sniffed type '" + + type + + "'. " + + "Should be '" + + expectedType + + "'. " + + "Header is ['" + + contentTypeHeaderList[headerIdx][0] + + "', " + + contentTypeHeaderList[headerIdx][1] + + "]. " + + "Body is ['" + + bodyList[bodyIdx][0].toSource() + + "', " + + bodyList[bodyIdx][1] + + "]." + ); + } + Assert.equal(expectedType, type); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + // Advance to next test + ++headerIdx; + if (headerIdx == contentTypeHeaderList.length) { + headerIdx = 0; + ++bodyIdx; + } + + if (bodyIdx == bodyList.length) { + do_test_pending(); + httpserv.stop(do_test_finished); + } else { + doTest(headerIdx, bodyIdx); + } + + do_test_finished(); + }, + }; + + return listener; +} + +function doTest(headerIdx, bodyIdx) { + var chan = makeChan(headerIdx, bodyIdx); + + var listener = makeListener(headerIdx, bodyIdx); + + chan.asyncOpen(listener); + + do_test_pending(); +} + +function createResponse(headerIdx, bodyIdx, metadata, response) { + response.setHeader( + "Content-Type", + contentTypeHeaderList[headerIdx][0], + false + ); + response.bodyOutputStream.write( + bodyList[bodyIdx][0], + bodyList[bodyIdx][0].length + ); +} + +function makeHandler(headerIdx, bodyIdx) { + var f = function handlerClosure(metadata, response) { + return createResponse(headerIdx, bodyIdx, metadata, response); + }; + return f; +} + +var httpserv; +function run_test() { + // disable on Windows for now, because it seems to leak sockets and die. + // Silly operating system! + // This is a really nasty way to detect Windows. I wish we could do better. + if (mozinfo.os == "win") { + //failing eslint no-empty test + } + + httpserv = new HttpServer(); + + for (i = 0; i < contentTypeHeaderList.length; ++i) { + for (j = 0; j < bodyList.length; ++j) { + httpserv.registerPathHandler("/" + i + "/" + j, makeHandler(i, j)); + } + } + + httpserv.start(-1); + + doTest(0, 0); +} diff --git a/netwerk/test/unit/test_port_remapping.js b/netwerk/test/unit/test_port_remapping.js new file mode 100644 index 0000000000..537d8d6f46 --- /dev/null +++ b/netwerk/test/unit/test_port_remapping.js @@ -0,0 +1,48 @@ +// This test is checking the `network.socket.forcePort` preference has an effect. +// We remap an ilusional port `8765` to go to the port the server actually binds to. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const REMAPPED_PORT = 8765; + +add_task(async function check_protocols() { + function contentHandler(metadata, response) { + let responseBody = "The server should never return this!"; + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); + } + + const httpserv = new HttpServer(); + httpserv.registerPathHandler("/content", contentHandler); + httpserv.start(-1); + + do_get_profile(); + Services.prefs.setCharPref( + "network.socket.forcePort", + `${REMAPPED_PORT}=${httpserv.identity.primaryPort}` + ); + + function get_response() { + return new Promise(resolve => { + const URL = `http://localhost:${REMAPPED_PORT}/content`; + const channel = make_channel(URL); + channel.asyncOpen( + new ChannelListener((request, data) => { + resolve(data); + }) + ); + }); + } + + // We expect "Bad request" from the test server because the server doesn't + // have identity for the remapped port. We don't want to add it too, because + // that would not prove we actualy remap the port number. + Assert.equal(await get_response(), "Bad request\n"); + await new Promise(resolve => httpserv.stop(resolve)); +}); diff --git a/netwerk/test/unit/test_post.js b/netwerk/test/unit/test_post.js new file mode 100644 index 0000000000..edb68f6e43 --- /dev/null +++ b/netwerk/test/unit/test_post.js @@ -0,0 +1,139 @@ +// +// POST test +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var testpath = "/simple"; + +var testfile = do_get_file("../unit/data/test_readline6.txt"); + +const BOUNDARY = "AaB03x"; +var teststring1 = + "--" + + BOUNDARY + + "\r\n" + + 'Content-Disposition: form-data; name="body"\r\n\r\n' + + "0123456789\r\n" + + "--" + + BOUNDARY + + "\r\n" + + 'Content-Disposition: form-data; name="files"; filename="' + + testfile.leafName + + '"\r\n' + + "Content-Type: application/octet-stream\r\n" + + "Content-Length: " + + testfile.fileSize + + "\r\n\r\n"; +var teststring2 = "--" + BOUNDARY + "--\r\n"; + +const BUFFERSIZE = 4096; +var correctOnProgress = false; + +var listenerCallback = { + QueryInterface: ChromeUtils.generateQI(["nsIProgressEventSink"]), + + getInterface(iid) { + if (iid.equals(Ci.nsIProgressEventSink)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + onProgress(request, progress, progressMax) { + // this works because the response is 0 bytes and does not trigger onprogress + if (progress === progressMax) { + correctOnProgress = true; + } + }, + + onStatus(request, status, statusArg) {}, +}; + +function run_test() { + var sstream1 = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + sstream1.data = teststring1; + + var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(testfile, -1, -1, 0); + + var buffered = Cc[ + "@mozilla.org/network/buffered-input-stream;1" + ].createInstance(Ci.nsIBufferedInputStream); + buffered.init(fstream, BUFFERSIZE); + + var sstream2 = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + sstream2.data = teststring2; + + var multi = Cc["@mozilla.org/io/multiplex-input-stream;1"].createInstance( + Ci.nsIMultiplexInputStream + ); + multi.appendStream(sstream1); + multi.appendStream(buffered); + multi.appendStream(sstream2); + + var mime = Cc["@mozilla.org/network/mime-input-stream;1"].createInstance( + Ci.nsIMIMEInputStream + ); + mime.addHeader("Content-Type", "multipart/form-data; boundary=" + BOUNDARY); + mime.setData(multi); + + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + + var channel = setupChannel(testpath); + + channel + .QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(mime, "", mime.available()); + channel.requestMethod = "POST"; + channel.notificationCallbacks = listenerCallback; + channel.asyncOpen(new ChannelListener(checkRequest, channel)); + do_test_pending(); +} + +function setupChannel(path) { + return NetUtil.newChannel({ + uri: URL + path, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function serverHandler(metadata, response) { + Assert.equal(metadata.method, "POST"); + + var data = read_stream( + metadata.bodyInputStream, + metadata.bodyInputStream.available() + ); + + var testfile_stream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + testfile_stream.init(testfile, -1, -1, 0); + + Assert.equal( + teststring1 + + read_stream(testfile_stream, testfile_stream.available()) + + teststring2, + data + ); +} + +function checkRequest(request, data, context) { + Assert.ok(correctOnProgress); + httpserver.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_predictor.js b/netwerk/test/unit/test_predictor.js new file mode 100644 index 0000000000..a5b8b4440a --- /dev/null +++ b/netwerk/test/unit/test_predictor.js @@ -0,0 +1,850 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +var running_single_process = false; + +var predictor = null; + +function is_child_process() { + return Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT; +} + +function extract_origin(uri) { + var o = uri.scheme + "://" + uri.asciiHost; + if (uri.port !== -1) { + o = o + ":" + uri.port; + } + return o; +} + +var origin_attributes = {}; + +var ValidityChecker = function (verifier, httpStatus) { + this.verifier = verifier; + this.httpStatus = httpStatus; +}; + +ValidityChecker.prototype = { + verifier: null, + httpStatus: 0, + + QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), + + onCacheEntryCheck(entry) { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + + onCacheEntryAvailable(entry, isnew, status) { + // Check if forced valid + Assert.equal(entry.isForcedValid, this.httpStatus === 200); + this.verifier.maybe_run_next_test(); + }, +}; + +var Verifier = function _verifier( + testing, + expected_prefetches, + expected_preconnects, + expected_preresolves +) { + this.verifying = testing; + this.expected_prefetches = expected_prefetches; + this.expected_preconnects = expected_preconnects; + this.expected_preresolves = expected_preresolves; +}; + +Verifier.prototype = { + complete: false, + verifying: null, + expected_prefetches: null, + expected_preconnects: null, + expected_preresolves: null, + + getInterface: function verifier_getInterface(iid) { + return this.QueryInterface(iid); + }, + + QueryInterface: ChromeUtils.generateQI(["nsINetworkPredictorVerifier"]), + + maybe_run_next_test: function verifier_maybe_run_next_test() { + if ( + this.expected_prefetches.length === 0 && + this.expected_preconnects.length === 0 && + this.expected_preresolves.length === 0 && + !this.complete + ) { + this.complete = true; + Assert.ok(true, "Well this is unexpected..."); + // This kicks off the ability to run the next test + reset_predictor(); + } + }, + + onPredictPrefetch: function verifier_onPredictPrefetch(uri, status) { + var index = this.expected_prefetches.indexOf(uri.asciiSpec); + if (index == -1 && !this.complete) { + Assert.ok(false, "Got prefetch for unexpected uri " + uri.asciiSpec); + } else { + this.expected_prefetches.splice(index, 1); + } + + dump("checking validity of entry for " + uri.spec + "\n"); + var checker = new ValidityChecker(this, status); + asyncOpenCacheEntry( + uri.spec, + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + checker + ); + }, + + onPredictPreconnect: function verifier_onPredictPreconnect(uri) { + var origin = extract_origin(uri); + var index = this.expected_preconnects.indexOf(origin); + if (index == -1 && !this.complete) { + Assert.ok(false, "Got preconnect for unexpected uri " + origin); + } else { + this.expected_preconnects.splice(index, 1); + } + this.maybe_run_next_test(); + }, + + onPredictDNS: function verifier_onPredictDNS(uri) { + var origin = extract_origin(uri); + var index = this.expected_preresolves.indexOf(origin); + if (index == -1 && !this.complete) { + Assert.ok(false, "Got preresolve for unexpected uri " + origin); + } else { + this.expected_preresolves.splice(index, 1); + } + this.maybe_run_next_test(); + }, +}; + +function reset_predictor() { + if (running_single_process || is_child_process()) { + predictor.reset(); + } else { + sendCommand("predictor.reset();"); + } +} + +function newURI(s) { + return Services.io.newURI(s); +} + +var prepListener = { + numEntriesToOpen: 0, + numEntriesOpened: 0, + continueCallback: null, + + QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), + + init(entriesToOpen, cb) { + this.numEntriesOpened = 0; + this.numEntriesToOpen = entriesToOpen; + this.continueCallback = cb; + }, + + onCacheEntryCheck(entry) { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + + onCacheEntryAvailable(entry, isNew, result) { + Assert.equal(result, Cr.NS_OK); + entry.setMetaDataElement("predictor_test", "1"); + entry.metaDataReady(); + this.numEntriesOpened++; + if (this.numEntriesToOpen == this.numEntriesOpened) { + this.continueCallback(); + } + }, +}; + +function open_and_continue(uris, continueCallback) { + var ds = Services.cache2.diskCacheStorage(Services.loadContextInfo.default); + + prepListener.init(uris.length, continueCallback); + for (var i = 0; i < uris.length; ++i) { + ds.asyncOpenURI( + uris[i], + "", + Ci.nsICacheStorage.OPEN_NORMALLY, + prepListener + ); + } +} + +function test_link_hover() { + if (!running_single_process && !is_child_process()) { + // This one we can just proxy to the child and be done with, no extra setup + // is necessary. + sendCommand("test_link_hover();"); + return; + } + + var uri = newURI("http://localhost:4444/foo/bar"); + var referrer = newURI("http://localhost:4444/foo"); + var preconns = ["http://localhost:4444"]; + + var verifier = new Verifier("hover", [], preconns, []); + predictor.predict( + uri, + referrer, + predictor.PREDICT_LINK, + origin_attributes, + verifier + ); +} + +const pageload_toplevel = newURI("http://localhost:4444/index.html"); + +function continue_test_pageload() { + var subresources = [ + "http://localhost:4444/style.css", + "http://localhost:4443/jquery.js", + "http://localhost:4444/image.png", + ]; + + // This is necessary to learn the origin stuff + predictor.learn( + pageload_toplevel, + null, + predictor.LEARN_LOAD_TOPLEVEL, + origin_attributes + ); + do_timeout(0, () => { + // allow the learn() to run on the main thread + var preconns = []; + + var sruri = newURI(subresources[0]); + predictor.learn( + sruri, + pageload_toplevel, + predictor.LEARN_LOAD_SUBRESOURCE, + origin_attributes + ); + do_timeout(0, () => { + preconns.push(extract_origin(sruri)); + + sruri = newURI(subresources[1]); + predictor.learn( + sruri, + pageload_toplevel, + predictor.LEARN_LOAD_SUBRESOURCE, + origin_attributes + ); + do_timeout(0, () => { + preconns.push(extract_origin(sruri)); + + sruri = newURI(subresources[2]); + predictor.learn( + sruri, + pageload_toplevel, + predictor.LEARN_LOAD_SUBRESOURCE, + origin_attributes + ); + do_timeout(0, () => { + preconns.push(extract_origin(sruri)); + + var verifier = new Verifier("pageload", [], preconns, []); + predictor.predict( + pageload_toplevel, + null, + predictor.PREDICT_LOAD, + origin_attributes, + verifier + ); + }); + }); + }); + }); +} + +function test_pageload() { + open_and_continue([pageload_toplevel], function () { + if (running_single_process) { + continue_test_pageload(); + } else { + sendCommand("continue_test_pageload();"); + } + }); +} + +const redirect_inituri = newURI("http://localhost:4443/redirect"); +const redirect_targeturi = newURI("http://localhost:4444/index.html"); + +function continue_test_redirect() { + var subresources = [ + "http://localhost:4444/style.css", + "http://localhost:4443/jquery.js", + "http://localhost:4444/image.png", + ]; + + predictor.learn( + redirect_inituri, + null, + predictor.LEARN_LOAD_TOPLEVEL, + origin_attributes + ); + do_timeout(0, () => { + predictor.learn( + redirect_targeturi, + null, + predictor.LEARN_LOAD_TOPLEVEL, + origin_attributes + ); + do_timeout(0, () => { + predictor.learn( + redirect_targeturi, + redirect_inituri, + predictor.LEARN_LOAD_REDIRECT, + origin_attributes + ); + do_timeout(0, () => { + var preconns = []; + preconns.push(extract_origin(redirect_targeturi)); + + var sruri = newURI(subresources[0]); + predictor.learn( + sruri, + redirect_targeturi, + predictor.LEARN_LOAD_SUBRESOURCE, + origin_attributes + ); + do_timeout(0, () => { + preconns.push(extract_origin(sruri)); + + sruri = newURI(subresources[1]); + predictor.learn( + sruri[1], + redirect_targeturi, + predictor.LEARN_LOAD_SUBRESOURCE, + origin_attributes + ); + do_timeout(0, () => { + preconns.push(extract_origin(sruri)); + + sruri = newURI(subresources[2]); + predictor.learn( + sruri[2], + redirect_targeturi, + predictor.LEARN_LOAD_SUBRESOURCE, + origin_attributes + ); + do_timeout(0, () => { + preconns.push(extract_origin(sruri)); + + var verifier = new Verifier("redirect", [], preconns, []); + predictor.predict( + redirect_inituri, + null, + predictor.PREDICT_LOAD, + origin_attributes, + verifier + ); + }); + }); + }); + }); + }); + }); +} + +// Test is currently disabled. +// eslint-disable-next-line no-unused-vars +function test_redirect() { + open_and_continue([redirect_inituri, redirect_targeturi], function () { + if (running_single_process) { + continue_test_redirect(); + } else { + sendCommand("continue_test_redirect();"); + } + }); +} + +// Test is currently disabled. +// eslint-disable-next-line no-unused-vars +function test_startup() { + if (!running_single_process && !is_child_process()) { + // This one we can just proxy to the child and be done with, no extra setup + // is necessary. + sendCommand("test_startup();"); + return; + } + + var uris = ["http://localhost:4444/startup", "http://localhost:4443/startup"]; + var preconns = []; + var uri = newURI(uris[0]); + predictor.learn(uri, null, predictor.LEARN_STARTUP, origin_attributes); + do_timeout(0, () => { + preconns.push(extract_origin(uri)); + + uri = newURI(uris[1]); + predictor.learn(uri, null, predictor.LEARN_STARTUP, origin_attributes); + do_timeout(0, () => { + preconns.push(extract_origin(uri)); + + var verifier = new Verifier("startup", [], preconns, []); + predictor.predict( + null, + null, + predictor.PREDICT_STARTUP, + origin_attributes, + verifier + ); + }); + }); +} + +const dns_toplevel = newURI("http://localhost:4444/index.html"); + +function continue_test_dns() { + var subresource = "http://localhost:4443/jquery.js"; + + predictor.learn( + dns_toplevel, + null, + predictor.LEARN_LOAD_TOPLEVEL, + origin_attributes + ); + do_timeout(0, () => { + var sruri = newURI(subresource); + predictor.learn( + sruri, + dns_toplevel, + predictor.LEARN_LOAD_SUBRESOURCE, + origin_attributes + ); + do_timeout(0, () => { + var preresolves = [extract_origin(sruri)]; + var verifier = new Verifier("dns", [], [], preresolves); + predictor.predict( + dns_toplevel, + null, + predictor.PREDICT_LOAD, + origin_attributes, + verifier + ); + }); + }); +} + +function test_dns() { + open_and_continue([dns_toplevel], function () { + // Ensure that this will do preresolves + Services.prefs.setIntPref( + "network.predictor.preconnect-min-confidence", + 101 + ); + if (running_single_process) { + continue_test_dns(); + } else { + sendCommand("continue_test_dns();"); + } + }); +} + +const origin_toplevel = newURI("http://localhost:4444/index.html"); + +function continue_test_origin() { + var subresources = [ + "http://localhost:4444/style.css", + "http://localhost:4443/jquery.js", + "http://localhost:4444/image.png", + ]; + predictor.learn( + origin_toplevel, + null, + predictor.LEARN_LOAD_TOPLEVEL, + origin_attributes + ); + do_timeout(0, () => { + var preconns = []; + + var sruri = newURI(subresources[0]); + predictor.learn( + sruri, + origin_toplevel, + predictor.LEARN_LOAD_SUBRESOURCE, + origin_attributes + ); + do_timeout(0, () => { + var origin = extract_origin(sruri); + if (!preconns.includes(origin)) { + preconns.push(origin); + } + + sruri = newURI(subresources[1]); + predictor.learn( + sruri, + origin_toplevel, + predictor.LEARN_LOAD_SUBRESOURCE, + origin_attributes + ); + do_timeout(0, () => { + var origin = extract_origin(sruri); + if (!preconns.includes(origin)) { + preconns.push(origin); + } + + sruri = newURI(subresources[2]); + predictor.learn( + sruri, + origin_toplevel, + predictor.LEARN_LOAD_SUBRESOURCE, + origin_attributes + ); + do_timeout(0, () => { + var origin = extract_origin(sruri); + if (!preconns.includes(origin)) { + preconns.push(origin); + } + + var loaduri = newURI("http://localhost:4444/anotherpage.html"); + var verifier = new Verifier("origin", [], preconns, []); + predictor.predict( + loaduri, + null, + predictor.PREDICT_LOAD, + origin_attributes, + verifier + ); + }); + }); + }); + }); +} + +function test_origin() { + open_and_continue([origin_toplevel], function () { + if (running_single_process) { + continue_test_origin(); + } else { + sendCommand("continue_test_origin();"); + } + }); +} + +var httpserv = null; +var prefetch_tluri; +var prefetch_sruri; + +function prefetchHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + var body = "Success (meow meow meow)."; + + response.bodyOutputStream.write(body, body.length); +} + +var prefetchListener = { + onStartRequest(request) { + Assert.equal(request.status, Cr.NS_OK); + }, + + onDataAvailable(request, stream, offset, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest(request, status) { + run_next_test(); + }, +}; + +function test_prefetch_setup() { + // Disable preconnects and preresolves + Services.prefs.setIntPref("network.predictor.preconnect-min-confidence", 101); + Services.prefs.setIntPref("network.predictor.preresolve-min-confidence", 101); + + Services.prefs.setBoolPref("network.predictor.enable-prefetch", true); + + // Makes it so we only have to call test_prefetch_prime twice to make prefetch + // do its thing. + Services.prefs.setIntPref("network.predictor.prefetch-rolling-load-count", 2); + + // This test does not run in e10s-mode, so we'll just go ahead and skip it. + // We've left the e10s test code in below, just in case someone wants to try + // to make it work at some point in the future. + if (!running_single_process) { + dump("skipping test_prefetch_setup due to e10s\n"); + run_next_test(); + return; + } + + httpserv = new HttpServer(); + httpserv.registerPathHandler("/cat.jpg", prefetchHandler); + httpserv.start(-1); + + var tluri = + "http://127.0.0.1:" + httpserv.identity.primaryPort + "/index.html"; + var sruri = "http://127.0.0.1:" + httpserv.identity.primaryPort + "/cat.jpg"; + prefetch_tluri = newURI(tluri); + prefetch_sruri = newURI(sruri); + if (!running_single_process && !is_child_process()) { + // Give the child process access to these values + sendCommand('prefetch_tluri = newURI("' + tluri + '");'); + sendCommand('prefetch_sruri = newURI("' + sruri + '");'); + } + + run_next_test(); +} + +// Used to "prime the pump" for prefetch - it makes sure all our learns go +// through as expected so that prefetching will happen. +function test_prefetch_prime() { + // This test does not run in e10s-mode, so we'll just go ahead and skip it. + // We've left the e10s test code in below, just in case someone wants to try + // to make it work at some point in the future. + if (!running_single_process) { + dump("skipping test_prefetch_prime due to e10s\n"); + run_next_test(); + return; + } + + open_and_continue([prefetch_tluri], function () { + if (running_single_process) { + predictor.learn( + prefetch_tluri, + null, + predictor.LEARN_LOAD_TOPLEVEL, + origin_attributes + ); + predictor.learn( + prefetch_sruri, + prefetch_tluri, + predictor.LEARN_LOAD_SUBRESOURCE, + origin_attributes + ); + } else { + sendCommand( + "predictor.learn(prefetch_tluri, null, predictor.LEARN_LOAD_TOPLEVEL, origin_attributes);" + ); + sendCommand( + "predictor.learn(prefetch_sruri, prefetch_tluri, predictor.LEARN_LOAD_SUBRESOURCE, origin_attributes);" + ); + } + + // This runs in the parent or only process + var channel = NetUtil.newChannel({ + uri: prefetch_sruri.asciiSpec, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + channel.requestMethod = "GET"; + channel.referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.EMPTY, + true, + prefetch_tluri + ); + channel.asyncOpen(prefetchListener); + }); +} + +function test_prefetch() { + // This test does not run in e10s-mode, so we'll just go ahead and skip it. + // We've left the e10s test code in below, just in case someone wants to try + // to make it work at some point in the future. + if (!running_single_process) { + dump("skipping test_prefetch due to e10s\n"); + run_next_test(); + return; + } + + // Setup for this has all been taken care of by test_prefetch_prime, so we can + // continue on without pausing here. + if (running_single_process) { + continue_test_prefetch(); + } else { + sendCommand("continue_test_prefetch();"); + } +} + +function continue_test_prefetch() { + var prefetches = [prefetch_sruri.asciiSpec]; + var verifier = new Verifier("prefetch", prefetches, [], []); + predictor.predict( + prefetch_tluri, + null, + predictor.PREDICT_LOAD, + origin_attributes, + verifier + ); +} + +function test_visitor_doom() { + // See bug 1708673 + Services.prefs.setBoolPref("network.cache.bug1708673", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.cache.bug1708673"); + }); + + let p1 = new Promise(resolve => { + let doomTasks = []; + let visitor = { + onCacheStorageInfo() {}, + async onCacheEntryInfo( + aURI, + aIdEnhance, + aDataSize, + aAltDataSize, + aFetchCount, + aLastModifiedTime, + aExpirationTime, + aPinned, + aInfo + ) { + let storages = [ + Services.cache2.memoryCacheStorage(aInfo), + Services.cache2.diskCacheStorage(aInfo, false), + ]; + console.debug("asyncDoomURI", aURI.spec); + let doomTask = Promise.all( + storages.map(storage => { + return new Promise(resolve => { + storage.asyncDoomURI(aURI, aIdEnhance, { + onCacheEntryDoomed: resolve, + }); + }); + }) + ); + doomTasks.push(doomTask); + }, + onCacheEntryVisitCompleted() { + Promise.allSettled(doomTasks).then(resolve); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + Services.cache2.asyncVisitAllStorages(visitor, true); + }); + + let p2 = new Promise(resolve => { + reset_predictor(); + resolve(); + }); + + do_test_pending(); + Promise.allSettled([p1, p2]).then(() => { + return new Promise(resolve => { + let entryCount = 0; + let visitor = { + onCacheStorageInfo() {}, + async onCacheEntryInfo( + aURI, + aIdEnhance, + aDataSize, + aAltDataSize, + aFetchCount, + aLastModifiedTime, + aExpirationTime, + aPinned, + aInfo + ) { + entryCount++; + }, + onCacheEntryVisitCompleted() { + Assert.equal(entryCount, 0); + resolve(); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + Services.cache2.asyncVisitAllStorages(visitor, true); + }).then(run_next_test); + }); +} + +function cleanup() { + observer.cleaningUp = true; + if (running_single_process) { + // The http server is required (and started) by the prefetch test, which + // only runs in single-process mode, so don't try to shut it down if we're + // in e10s mode. + do_test_pending(); + httpserv.stop(do_test_finished); + } + reset_predictor(); +} + +var tests = [ + // This must ALWAYS come first, to ensure a clean slate + reset_predictor, + test_link_hover, + test_pageload, + // TODO: These are disabled until the features are re-written + //test_redirect, + //test_startup, + // END DISABLED TESTS + test_origin, + test_dns, + test_prefetch_setup, + test_prefetch_prime, + test_prefetch_prime, + test_prefetch, + test_visitor_doom, + // This must ALWAYS come last, to ensure we clean up after ourselves + cleanup, +]; + +var observer = { + cleaningUp: false, + + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(subject, topic, data) { + if (topic != "predictor-reset-complete") { + return; + } + + if (this.cleaningUp) { + unregisterObserver(); + } + + run_next_test(); + }, +}; + +function registerObserver() { + Services.obs.addObserver(observer, "predictor-reset-complete"); +} + +function unregisterObserver() { + Services.obs.removeObserver(observer, "predictor-reset-complete"); +} + +function run_test_real() { + tests.forEach(f => add_test(f)); + do_get_profile(); + + Services.prefs.setBoolPref("network.predictor.enabled", true); + Services.prefs.setBoolPref("network.predictor.doing-tests", true); + + predictor = Cc["@mozilla.org/network/predictor;1"].getService( + Ci.nsINetworkPredictor + ); + + registerObserver(); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.predictor.preconnect-min-confidence"); + Services.prefs.clearUserPref("network.predictor.enabled"); + Services.prefs.clearUserPref("network.predictor.preresolve-min-confidence"); + Services.prefs.clearUserPref("network.predictor.enable-prefetch"); + Services.prefs.clearUserPref( + "network.predictor.prefetch-rolling-load-count" + ); + Services.prefs.clearUserPref("network.predictor.doing-tests"); + }); + + run_next_test(); +} + +function run_test() { + // This indirection is necessary to make e10s tests work as expected + running_single_process = true; + run_test_real(); +} diff --git a/netwerk/test/unit/test_private_cookie_changed.js b/netwerk/test/unit/test_private_cookie_changed.js new file mode 100644 index 0000000000..89e9f5e75a --- /dev/null +++ b/netwerk/test/unit/test_private_cookie_changed.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +function makeChan(uri, isPrivate) { + var chan = NetUtil.newChannel({ + uri: uri.spec, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + + chan.QueryInterface(Ci.nsIPrivateBrowsingChannel).setPrivate(isPrivate); + return chan; +} + +function run_test() { + // We don't want to have CookieJarSettings blocking this test. + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + let publicNotifications = 0; + let privateNotifications = 0; + Services.obs.addObserver(function () { + publicNotifications++; + }, "cookie-changed"); + Services.obs.addObserver(function () { + privateNotifications++; + }, "private-cookie-changed"); + + let uri = NetUtil.newURI("http://foo.com/"); + let publicChan = makeChan(uri, false); + let svc = Services.cookies.QueryInterface(Ci.nsICookieService); + svc.setCookieStringFromHttp(uri, "oh=hai", publicChan); + let privateChan = makeChan(uri, true); + svc.setCookieStringFromHttp(uri, "oh=hai", privateChan); + Assert.equal(publicNotifications, 1); + Assert.equal(privateNotifications, 1); +} diff --git a/netwerk/test/unit/test_private_necko_channel.js b/netwerk/test/unit/test_private_necko_channel.js new file mode 100644 index 0000000000..d663d332e8 --- /dev/null +++ b/netwerk/test/unit/test_private_necko_channel.js @@ -0,0 +1,58 @@ +// +// Private channel test +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var testpath = "/simple"; +var httpbody = "0123456789"; + +function run_test() { + // Simulate a profile dir for xpcshell + do_get_profile(); + + // Start off with an empty cache + evict_cache_entries(); + + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + + var channel = setupChannel(testpath); + channel.loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance(); + + channel.QueryInterface(Ci.nsIPrivateBrowsingChannel); + channel.setPrivate(true); + + channel.asyncOpen(new ChannelListener(checkRequest, channel)); + + do_test_pending(); +} + +function setupChannel(path) { + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + path, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function serverHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(httpbody, httpbody.length); +} + +function checkRequest(request, data, context) { + get_device_entry_count("disk", null, function (count) { + Assert.equal(count, 0); + get_device_entry_count( + "disk", + Services.loadContextInfo.private, + function (count) { + Assert.equal(count, 1); + httpserver.stop(do_test_finished); + } + ); + }); +} diff --git a/netwerk/test/unit/test_progress.js b/netwerk/test/unit/test_progress.js new file mode 100644 index 0000000000..3141b7df94 --- /dev/null +++ b/netwerk/test/unit/test_progress.js @@ -0,0 +1,143 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var testpath = "/simple"; +var httpbody = "0123456789"; + +var last = 0, + max = 0; + +const STATUS_RECEIVING_FROM = 0x4b0006; +const LOOPS = 50000; + +const TYPE_ONSTATUS = 1; +const TYPE_ONPROGRESS = 2; +const TYPE_ONSTARTREQUEST = 3; +const TYPE_ONDATAAVAILABLE = 4; +const TYPE_ONSTOPREQUEST = 5; + +var ProgressCallback = function () {}; + +ProgressCallback.prototype = { + _listener: null, + _got_onstartrequest: false, + _got_onstatus_after_onstartrequest: false, + _last_callback_handled: null, + statusArg: "", + finish: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIProgressEventSink", + "nsIStreamListener", + "nsIRequestObserver", + ]), + + getInterface(iid) { + if ( + iid.equals(Ci.nsIProgressEventSink) || + iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver) + ) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + onStartRequest(request) { + Assert.equal(this._last_callback_handled, TYPE_ONSTATUS); + this._got_onstartrequest = true; + this._last_callback_handled = TYPE_ONSTARTREQUEST; + + this._listener = new ChannelListener(checkRequest, request); + this._listener.onStartRequest(request); + }, + + onDataAvailable(request, data, offset, count) { + Assert.equal(this._last_callback_handled, TYPE_ONPROGRESS); + this._last_callback_handled = TYPE_ONDATAAVAILABLE; + + this._listener.onDataAvailable(request, data, offset, count); + }, + + onStopRequest(request, status) { + Assert.equal(this._last_callback_handled, TYPE_ONDATAAVAILABLE); + Assert.ok(this._got_onstatus_after_onstartrequest); + this._last_callback_handled = TYPE_ONSTOPREQUEST; + + this._listener.onStopRequest(request, status); + delete this._listener; + this.finish(); + }, + + onProgress(request, progress, progressMax) { + Assert.equal(this._last_callback_handled, TYPE_ONSTATUS); + this._last_callback_handled = TYPE_ONPROGRESS; + + Assert.equal(this.mStatus, STATUS_RECEIVING_FROM); + last = progress; + max = progressMax; + }, + + onStatus(request, status, statusArg) { + if (!this._got_onstartrequest) { + // Ensure that all messages before onStartRequest are onStatus + if (this._last_callback_handled) { + Assert.equal(this._last_callback_handled, TYPE_ONSTATUS); + } + } else if (this._last_callback_handled == TYPE_ONSTARTREQUEST) { + this._got_onstatus_after_onstartrequest = true; + } else { + Assert.equal(this._last_callback_handled, TYPE_ONDATAAVAILABLE); + } + this._last_callback_handled = TYPE_ONSTATUS; + + Assert.equal(statusArg, this.statusArg); + this.mStatus = status; + }, + + mStatus: 0, +}; + +registerCleanupFunction(async () => { + await httpserver.stop(); +}); + +function chanPromise(uri, statusArg) { + return new Promise(resolve => { + var chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + let listener = new ProgressCallback(); + listener.statusArg = statusArg; + chan.notificationCallbacks = listener; + listener.finish = resolve; + chan.asyncOpen(listener); + }); +} + +add_task(async function test_http1_1() { + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + await chanPromise(URL + testpath, "localhost"); +}); + +function serverHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + for (let i = 0; i < LOOPS; i++) { + response.bodyOutputStream.write(httpbody, httpbody.length); + } +} + +function checkRequest(request, data, context) { + Assert.equal(last, httpbody.length * LOOPS); + Assert.equal(max, httpbody.length * LOOPS); +} diff --git a/netwerk/test/unit/test_progress_no_proxy_and_proxy.js b/netwerk/test/unit/test_progress_no_proxy_and_proxy.js new file mode 100644 index 0000000000..cb132360c8 --- /dev/null +++ b/netwerk/test/unit/test_progress_no_proxy_and_proxy.js @@ -0,0 +1,205 @@ +/* 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"; + +// This test can be merged with test_progress.js once HTTP/3 tests are +// enabled on all plaforms. + +var last = 0; +var max = 0; +var using_proxy = false; + +const RESPONSE_LENGTH = 3000000; +const STATUS_RECEIVING_FROM = 0x4b0006; + +const TYPE_ONSTATUS = 1; +const TYPE_ONPROGRESS = 2; +const TYPE_ONSTARTREQUEST = 3; +const TYPE_ONDATAAVAILABLE = 4; +const TYPE_ONSTOPREQUEST = 5; + +var ProgressCallback = function () {}; + +ProgressCallback.prototype = { + _listener: null, + _got_onstartrequest: false, + _got_onstatus_after_onstartrequest: false, + _last_callback_handled: null, + statusArg: "", + finish: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIProgressEventSink", + "nsIStreamListener", + "nsIRequestObserver", + ]), + + getInterface(iid) { + if ( + iid.equals(Ci.nsIProgressEventSink) || + iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver) + ) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + onStartRequest(request) { + Assert.equal(this._last_callback_handled, TYPE_ONSTATUS); + this._got_onstartrequest = true; + this._last_callback_handled = TYPE_ONSTARTREQUEST; + + this._listener = new ChannelListener(checkRequest, request); + this._listener.onStartRequest(request); + }, + + onDataAvailable(request, data, offset, count) { + Assert.equal(this._last_callback_handled, TYPE_ONPROGRESS); + this._last_callback_handled = TYPE_ONDATAAVAILABLE; + + this._listener.onDataAvailable(request, data, offset, count); + }, + + onStopRequest(request, status) { + Assert.equal(this._last_callback_handled, TYPE_ONDATAAVAILABLE); + Assert.ok(this._got_onstatus_after_onstartrequest); + this._last_callback_handled = TYPE_ONSTOPREQUEST; + + this._listener.onStopRequest(request, status); + delete this._listener; + + check_http_info(request, this.expected_httpVersion, using_proxy); + + this.finish(); + }, + + onProgress(request, progress, progressMax) { + Assert.equal(this._last_callback_handled, TYPE_ONSTATUS); + this._last_callback_handled = TYPE_ONPROGRESS; + + Assert.equal(this.mStatus, STATUS_RECEIVING_FROM); + last = progress; + max = progressMax; + }, + + onStatus(request, status, statusArg) { + if (!this._got_onstartrequest) { + // Ensure that all messages before onStartRequest are onStatus + if (this._last_callback_handled) { + Assert.equal(this._last_callback_handled, TYPE_ONSTATUS); + } + } else if (this._last_callback_handled == TYPE_ONSTARTREQUEST) { + this._got_onstatus_after_onstartrequest = true; + } else { + Assert.equal(this._last_callback_handled, TYPE_ONDATAAVAILABLE); + } + this._last_callback_handled = TYPE_ONSTATUS; + + Assert.equal(statusArg, this.statusArg); + this.mStatus = status; + }, + + mStatus: 0, +}; + +function chanPromise(uri, statusArg, version) { + return new Promise(resolve => { + var chan = makeHTTPChannel(uri, using_proxy); + chan.requestMethod = "GET"; + let listener = new ProgressCallback(); + listener.statusArg = statusArg; + chan.notificationCallbacks = listener; + listener.expected_httpVersion = version; + listener.finish = resolve; + chan.asyncOpen(listener); + }); +} + +function checkRequest(request, data, context) { + Assert.equal(last, RESPONSE_LENGTH); + Assert.equal(max, RESPONSE_LENGTH); +} + +async function check_progress(server) { + info(`Testing ${server.constructor.name}`); + await server.registerPathHandler("/test", (req, resp) => { + // Generate a post. + function generateContent(size) { + return "0".repeat(size); + } + + resp.writeHead(200, { + "content-type": "application/json", + "content-length": "3000000", + }); + resp.end(generateContent(3000000)); + }); + await chanPromise( + `${server.origin()}/test`, + `${server.domain()}`, + server.version() + ); +} + +add_task(async function setup() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); +}); + +add_task(async function test_http_1_and_2() { + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + check_progress + ); +}); + +add_task(async function test_http_proxy() { + using_proxy = true; + let proxy = new NodeHTTPProxyServer(); + await proxy.start(); + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + check_progress + ); + await proxy.stop(); + using_proxy = false; +}); + +add_task(async function test_https_proxy() { + using_proxy = true; + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + check_progress + ); + await proxy.stop(); + using_proxy = false; +}); + +add_task(async function test_http2_proxy() { + using_proxy = true; + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + check_progress + ); + await proxy.stop(); + using_proxy = false; +}); + +add_task(async function test_http3() { + await http3_setup_tests("h3-29"); + await chanPromise( + "https://foo.example.com/" + RESPONSE_LENGTH, + "foo.example.com", + "h3-29" + ); + http3_clear_prefs(); +}); diff --git a/netwerk/test/unit/test_protocolproxyservice-async-filters.js b/netwerk/test/unit/test_protocolproxyservice-async-filters.js new file mode 100644 index 0000000000..fcf43d63ef --- /dev/null +++ b/netwerk/test/unit/test_protocolproxyservice-async-filters.js @@ -0,0 +1,435 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 testcase exercises the Protocol Proxy Service's async filter functionality +// run_filter_*() are entry points for each individual test. + +"use strict"; + +var pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + +/** + * Test nsIProtocolHandler that allows proxying, but doesn't allow HTTP + * proxying. + */ +function TestProtocolHandler() {} +TestProtocolHandler.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIProtocolHandler"]), + scheme: "moz-test", + defaultPort: -1, + protocolFlags: + Ci.nsIProtocolHandler.URI_NOAUTH | + Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.ALLOWS_PROXY | + Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD, + newChannel(uri, aLoadInfo) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + allowPort(port, scheme) { + return true; + }, +}; + +function TestProtocolHandlerFactory() {} +TestProtocolHandlerFactory.prototype = { + createInstance(iid) { + return new TestProtocolHandler().QueryInterface(iid); + }, +}; + +function register_test_protocol_handler() { + var reg = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + reg.registerFactory( + Components.ID("{4ea7dd3a-8cae-499c-9f18-e1de773ca25b}"), + "TestProtocolHandler", + "@mozilla.org/network/protocol;1?name=moz-test", + new TestProtocolHandlerFactory() + ); +} + +function check_proxy(pi, type, host, port, flags, timeout, hasNext) { + Assert.notEqual(pi, null); + Assert.equal(pi.type, type); + Assert.equal(pi.host, host); + Assert.equal(pi.port, port); + if (flags != -1) { + Assert.equal(pi.flags, flags); + } + if (timeout != -1) { + Assert.equal(pi.failoverTimeout, timeout); + } + if (hasNext) { + Assert.notEqual(pi.failoverProxy, null); + } else { + Assert.equal(pi.failoverProxy, null); + } +} + +const SYNC = 0; +const THROW = 1; +const ASYNC = 2; + +function TestFilter(type, host, port, flags, timeout, result) { + this._type = type; + this._host = host; + this._port = port; + this._flags = flags; + this._timeout = timeout; + this._result = result; +} +TestFilter.prototype = { + _type: "", + _host: "", + _port: -1, + _flags: 0, + _timeout: 0, + _async: false, + _throwing: false, + + QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyFilter"]), + + applyFilter(uri, pi, cb) { + if (this._result == THROW) { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + + var pi_tail = pps.newProxyInfo( + this._type, + this._host, + this._port, + "", + "", + this._flags, + this._timeout, + null + ); + if (pi) { + pi.failoverProxy = pi_tail; + } else { + pi = pi_tail; + } + + if (this._result == ASYNC) { + executeSoon(() => { + cb.onProxyFilterResult(pi); + }); + } else { + cb.onProxyFilterResult(pi); + } + }, +}; + +function resolveCallback() {} +resolveCallback.prototype = { + nextFunction: null, + + QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyCallback"]), + + onProxyAvailable(req, channel, pi, status) { + this.nextFunction(pi); + }, +}; + +// ============================================================== + +var filter1; +var filter2; +var filter3; + +function run_filter_test1() { + filter1 = new TestFilter("http", "foo", 8080, 0, 10, ASYNC); + filter2 = new TestFilter("http", "bar", 8090, 0, 10, ASYNC); + pps.registerFilter(filter1, 20); + pps.registerFilter(filter2, 10); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test1_1; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test1_1(pi) { + check_proxy(pi, "http", "bar", 8090, 0, 10, true); + check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false); + + pps.unregisterFilter(filter2); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test1_2; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test1_2(pi) { + check_proxy(pi, "http", "foo", 8080, 0, 10, false); + + pps.unregisterFilter(filter1); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test1_3; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test1_3(pi) { + Assert.equal(pi, null); + run_filter2_sync_async(); +} + +function run_filter2_sync_async() { + filter1 = new TestFilter("http", "foo", 8080, 0, 10, SYNC); + filter2 = new TestFilter("http", "bar", 8090, 0, 10, ASYNC); + pps.registerFilter(filter1, 20); + pps.registerFilter(filter2, 10); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test2_1; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test2_1(pi) { + check_proxy(pi, "http", "bar", 8090, 0, 10, true); + check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false); + + pps.unregisterFilter(filter1); + pps.unregisterFilter(filter2); + + run_filter3_async_sync(); +} + +function run_filter3_async_sync() { + filter1 = new TestFilter("http", "foo", 8080, 0, 10, ASYNC); + filter2 = new TestFilter("http", "bar", 8090, 0, 10, SYNC); + pps.registerFilter(filter1, 20); + pps.registerFilter(filter2, 10); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test3_1; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test3_1(pi) { + check_proxy(pi, "http", "bar", 8090, 0, 10, true); + check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false); + + pps.unregisterFilter(filter1); + pps.unregisterFilter(filter2); + + run_filter4_throwing_sync_sync(); +} + +function run_filter4_throwing_sync_sync() { + filter1 = new TestFilter("", "", 0, 0, 0, THROW); + filter2 = new TestFilter("http", "foo", 8080, 0, 10, SYNC); + filter3 = new TestFilter("http", "bar", 8090, 0, 10, SYNC); + pps.registerFilter(filter1, 20); + pps.registerFilter(filter2, 10); + pps.registerFilter(filter3, 5); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test4_1; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla2.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test4_1(pi) { + check_proxy(pi, "http", "bar", 8090, 0, 10, true); + check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false); + + pps.unregisterFilter(filter1); + pps.unregisterFilter(filter2); + pps.unregisterFilter(filter3); + + run_filter5_sync_sync_throwing(); +} + +function run_filter5_sync_sync_throwing() { + filter1 = new TestFilter("http", "foo", 8080, 0, 10, SYNC); + filter2 = new TestFilter("http", "bar", 8090, 0, 10, SYNC); + filter3 = new TestFilter("", "", 0, 0, 0, THROW); + pps.registerFilter(filter1, 20); + pps.registerFilter(filter2, 10); + pps.registerFilter(filter3, 5); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test5_1; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test5_1(pi) { + check_proxy(pi, "http", "bar", 8090, 0, 10, true); + check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false); + + pps.unregisterFilter(filter1); + pps.unregisterFilter(filter2); + pps.unregisterFilter(filter3); + + run_filter5_2_throwing_async_async(); +} + +function run_filter5_2_throwing_async_async() { + filter1 = new TestFilter("", "", 0, 0, 0, THROW); + filter2 = new TestFilter("http", "foo", 8080, 0, 10, ASYNC); + filter3 = new TestFilter("http", "bar", 8090, 0, 10, ASYNC); + pps.registerFilter(filter1, 20); + pps.registerFilter(filter2, 10); + pps.registerFilter(filter3, 5); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test5_2; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test5_2(pi) { + check_proxy(pi, "http", "bar", 8090, 0, 10, true); + check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false); + + pps.unregisterFilter(filter1); + pps.unregisterFilter(filter2); + pps.unregisterFilter(filter3); + + run_filter6_async_async_throwing(); +} + +function run_filter6_async_async_throwing() { + filter1 = new TestFilter("http", "foo", 8080, 0, 10, ASYNC); + filter2 = new TestFilter("http", "bar", 8090, 0, 10, ASYNC); + filter3 = new TestFilter("", "", 0, 0, 0, THROW); + pps.registerFilter(filter1, 20); + pps.registerFilter(filter2, 10); + pps.registerFilter(filter3, 5); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test6_1; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test6_1(pi) { + check_proxy(pi, "http", "bar", 8090, 0, 10, true); + check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false); + + pps.unregisterFilter(filter1); + pps.unregisterFilter(filter2); + pps.unregisterFilter(filter3); + + run_filter7_async_throwing_async(); +} + +function run_filter7_async_throwing_async() { + filter1 = new TestFilter("http", "foo", 8080, 0, 10, ASYNC); + filter2 = new TestFilter("", "", 0, 0, 0, THROW); + filter3 = new TestFilter("http", "bar", 8090, 0, 10, ASYNC); + pps.registerFilter(filter1, 20); + pps.registerFilter(filter2, 10); + pps.registerFilter(filter3, 5); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test7_1; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test7_1(pi) { + check_proxy(pi, "http", "bar", 8090, 0, 10, true); + check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false); + + pps.unregisterFilter(filter1); + pps.unregisterFilter(filter2); + pps.unregisterFilter(filter3); + + run_filter8_sync_throwing_sync(); +} + +function run_filter8_sync_throwing_sync() { + filter1 = new TestFilter("http", "foo", 8080, 0, 10, SYNC); + filter2 = new TestFilter("", "", 0, 0, 0, THROW); + filter3 = new TestFilter("http", "bar", 8090, 0, 10, SYNC); + pps.registerFilter(filter1, 20); + pps.registerFilter(filter2, 10); + pps.registerFilter(filter3, 5); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test8_1; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test8_1(pi) { + check_proxy(pi, "http", "bar", 8090, 0, 10, true); + check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false); + + pps.unregisterFilter(filter1); + pps.unregisterFilter(filter2); + pps.unregisterFilter(filter3); + + run_filter9_throwing(); +} + +function run_filter9_throwing() { + filter1 = new TestFilter("", "", 0, 0, 0, THROW); + pps.registerFilter(filter1, 20); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test9_1; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test9_1(pi) { + Assert.equal(pi, null); + do_test_finished(); +} + +// ========================================= + +function run_test() { + register_test_protocol_handler(); + + // start of asynchronous test chain + run_filter_test1(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_protocolproxyservice.js b/netwerk/test/unit/test_protocolproxyservice.js new file mode 100644 index 0000000000..64472651f6 --- /dev/null +++ b/netwerk/test/unit/test_protocolproxyservice.js @@ -0,0 +1,1071 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 testcase exercises the Protocol Proxy Service + +// These are the major sub tests: +// run_filter_test(); +// run_filter_test2() +// run_filter_test3() +// run_pref_test(); +// run_pac_test(); +// run_pac_cancel_test(); +// run_proxy_host_filters_test(); +// run_myipaddress_test(); +// run_failed_script_test(); +// run_isresolvable_test(); + +"use strict"; + +var ios = Services.io; +var pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); +var prefs = Services.prefs; +var again = true; + +/** + * Test nsIProtocolHandler that allows proxying, but doesn't allow HTTP + * proxying. + */ +function TestProtocolHandler() {} +TestProtocolHandler.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIProtocolHandler"]), + scheme: "moz-test", + defaultPort: -1, + protocolFlags: + Ci.nsIProtocolHandler.URI_NOAUTH | + Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.ALLOWS_PROXY | + Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD, + newChannel(uri, aLoadInfo) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + allowPort(port, scheme) { + return true; + }, +}; + +function TestProtocolHandlerFactory() {} +TestProtocolHandlerFactory.prototype = { + createInstance(iid) { + return new TestProtocolHandler().QueryInterface(iid); + }, +}; + +function register_test_protocol_handler() { + var reg = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + reg.registerFactory( + Components.ID("{4ea7dd3a-8cae-499c-9f18-e1de773ca25b}"), + "TestProtocolHandler", + "@mozilla.org/network/protocol;1?name=moz-test", + new TestProtocolHandlerFactory() + ); +} + +function check_proxy(pi, type, host, port, flags, timeout, hasNext) { + Assert.notEqual(pi, null); + Assert.equal(pi.type, type); + Assert.equal(pi.host, host); + Assert.equal(pi.port, port); + if (flags != -1) { + Assert.equal(pi.flags, flags); + } + if (timeout != -1) { + Assert.equal(pi.failoverTimeout, timeout); + } + if (hasNext) { + Assert.notEqual(pi.failoverProxy, null); + } else { + Assert.equal(pi.failoverProxy, null); + } +} + +function TestFilter(type, host, port, flags, timeout) { + this._type = type; + this._host = host; + this._port = port; + this._flags = flags; + this._timeout = timeout; +} +TestFilter.prototype = { + _type: "", + _host: "", + _port: -1, + _flags: 0, + _timeout: 0, + QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyFilter"]), + applyFilter(uri, pi, cb) { + var pi_tail = pps.newProxyInfo( + this._type, + this._host, + this._port, + "", + "", + this._flags, + this._timeout, + null + ); + if (pi) { + pi.failoverProxy = pi_tail; + } else { + pi = pi_tail; + } + cb.onProxyFilterResult(pi); + }, +}; + +function BasicFilter() {} +BasicFilter.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyFilter"]), + applyFilter(uri, pi, cb) { + cb.onProxyFilterResult( + pps.newProxyInfo( + "http", + "localhost", + 8080, + "", + "", + 0, + 10, + pps.newProxyInfo("direct", "", -1, "", "", 0, 0, null) + ) + ); + }, +}; + +function BasicChannelFilter() {} +BasicChannelFilter.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyChannelFilter"]), + applyFilter(channel, pi, cb) { + cb.onProxyFilterResult( + pps.newProxyInfo( + "http", + channel.URI.host, + 7777, + "", + "", + 0, + 10, + pps.newProxyInfo("direct", "", -1, "", "", 0, 0, null) + ) + ); + }, +}; + +function resolveCallback() {} +resolveCallback.prototype = { + nextFunction: null, + + QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyCallback"]), + + onProxyAvailable(req, channel, pi, status) { + this.nextFunction(pi); + }, +}; + +function run_filter_test() { + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + + // Verify initial state + var cb = new resolveCallback(); + cb.nextFunction = filter_test0_1; + pps.asyncResolve(channel, 0, cb); +} + +var filter01; +var filter02; + +function filter_test0_1(pi) { + Assert.equal(pi, null); + + // Push a filter and verify the results + + filter01 = new BasicFilter(); + filter02 = new BasicFilter(); + pps.registerFilter(filter01, 10); + pps.registerFilter(filter02, 20); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test0_2; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test0_2(pi) { + check_proxy(pi, "http", "localhost", 8080, 0, 10, true); + check_proxy(pi.failoverProxy, "direct", "", -1, 0, 0, false); + + pps.unregisterFilter(filter02); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test0_3; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test0_3(pi) { + check_proxy(pi, "http", "localhost", 8080, 0, 10, true); + check_proxy(pi.failoverProxy, "direct", "", -1, 0, 0, false); + + // Remove filter and verify that we return to the initial state + + pps.unregisterFilter(filter01); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test0_4; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +var filter03; + +function filter_test0_4(pi) { + Assert.equal(pi, null); + filter03 = new BasicChannelFilter(); + pps.registerChannelFilter(filter03, 10); + var cb = new resolveCallback(); + cb.nextFunction = filter_test0_5; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test0_5(pi) { + pps.unregisterChannelFilter(filter03); + check_proxy(pi, "http", "www.mozilla.org", 7777, 0, 10, true); + check_proxy(pi.failoverProxy, "direct", "", -1, 0, 0, false); + run_filter_test_uri(); +} + +function run_filter_test_uri() { + var cb = new resolveCallback(); + cb.nextFunction = filter_test_uri0_1; + var uri = ios.newURI("http://www.mozilla.org/"); + pps.asyncResolve(uri, 0, cb); +} + +function filter_test_uri0_1(pi) { + Assert.equal(pi, null); + + // Push a filter and verify the results + + filter01 = new BasicFilter(); + filter02 = new BasicFilter(); + pps.registerFilter(filter01, 10); + pps.registerFilter(filter02, 20); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test_uri0_2; + var uri = ios.newURI("http://www.mozilla.org/"); + pps.asyncResolve(uri, 0, cb); +} + +function filter_test_uri0_2(pi) { + check_proxy(pi, "http", "localhost", 8080, 0, 10, true); + check_proxy(pi.failoverProxy, "direct", "", -1, 0, 0, false); + + pps.unregisterFilter(filter02); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test_uri0_3; + var uri = ios.newURI("http://www.mozilla.org/"); + pps.asyncResolve(uri, 0, cb); +} + +function filter_test_uri0_3(pi) { + check_proxy(pi, "http", "localhost", 8080, 0, 10, true); + check_proxy(pi.failoverProxy, "direct", "", -1, 0, 0, false); + + // Remove filter and verify that we return to the initial state + + pps.unregisterFilter(filter01); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test_uri0_4; + var uri = ios.newURI("http://www.mozilla.org/"); + pps.asyncResolve(uri, 0, cb); +} + +function filter_test_uri0_4(pi) { + Assert.equal(pi, null); + run_filter_test2(); +} + +var filter11; +var filter12; + +function run_filter_test2() { + // Push a filter and verify the results + + filter11 = new TestFilter("http", "foo", 8080, 0, 10); + filter12 = new TestFilter("http", "bar", 8090, 0, 10); + pps.registerFilter(filter11, 20); + pps.registerFilter(filter12, 10); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test1_1; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test1_1(pi) { + check_proxy(pi, "http", "bar", 8090, 0, 10, true); + check_proxy(pi.failoverProxy, "http", "foo", 8080, 0, 10, false); + + pps.unregisterFilter(filter12); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test1_2; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test1_2(pi) { + check_proxy(pi, "http", "foo", 8080, 0, 10, false); + + // Remove filter and verify that we return to the initial state + + pps.unregisterFilter(filter11); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test1_3; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function filter_test1_3(pi) { + Assert.equal(pi, null); + run_filter_test3(); +} + +var filter_3_1; + +function run_filter_test3() { + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + // Push a filter and verify the results asynchronously + + filter_3_1 = new TestFilter("http", "foo", 8080, 0, 10); + pps.registerFilter(filter_3_1, 20); + + var cb = new resolveCallback(); + cb.nextFunction = filter_test3_1; + pps.asyncResolve(channel, 0, cb); +} + +function filter_test3_1(pi) { + check_proxy(pi, "http", "foo", 8080, 0, 10, false); + pps.unregisterFilter(filter_3_1); + run_pref_test(); +} + +function run_pref_test() { + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + // Verify 'direct' setting + + prefs.setIntPref("network.proxy.type", 0); + + var cb = new resolveCallback(); + cb.nextFunction = pref_test1_1; + pps.asyncResolve(channel, 0, cb); +} + +function pref_test1_1(pi) { + Assert.equal(pi, null); + + // Verify 'manual' setting + prefs.setIntPref("network.proxy.type", 1); + + var cb = new resolveCallback(); + cb.nextFunction = pref_test1_2; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function pref_test1_2(pi) { + // nothing yet configured + Assert.equal(pi, null); + + // try HTTP configuration + prefs.setCharPref("network.proxy.http", "foopy"); + prefs.setIntPref("network.proxy.http_port", 8080); + + var cb = new resolveCallback(); + cb.nextFunction = pref_test1_3; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function pref_test1_3(pi) { + check_proxy(pi, "http", "foopy", 8080, 0, -1, false); + + prefs.setCharPref("network.proxy.http", ""); + prefs.setIntPref("network.proxy.http_port", 0); + + // try SOCKS configuration + prefs.setCharPref("network.proxy.socks", "barbar"); + prefs.setIntPref("network.proxy.socks_port", 1203); + + var cb = new resolveCallback(); + cb.nextFunction = pref_test1_4; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + pps.asyncResolve(channel, 0, cb); +} + +function pref_test1_4(pi) { + check_proxy(pi, "socks", "barbar", 1203, 0, -1, false); + run_pac_test(); +} + +function TestResolveCallback(type, nexttest) { + this.type = type; + this.nexttest = nexttest; +} +TestResolveCallback.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyCallback"]), + + onProxyAvailable: function TestResolveCallback_onProxyAvailable( + req, + channel, + pi, + status + ) { + dump("*** channelURI=" + channel.URI.spec + ", status=" + status + "\n"); + + if (this.type == null) { + Assert.equal(pi, null); + } else { + Assert.notEqual(req, null); + Assert.notEqual(channel, null); + Assert.equal(status, 0); + Assert.notEqual(pi, null); + check_proxy(pi, this.type, "foopy", 8080, 0, -1, true); + check_proxy(pi.failoverProxy, "direct", "", -1, -1, -1, false); + } + + this.nexttest(); + }, +}; + +var originalTLSProxy; + +function run_pac_test() { + var pac = + "data:text/plain," + + "function FindProxyForURL(url, host) {" + + ' return "PROXY foopy:8080; DIRECT";' + + "}"; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + // Configure PAC + + prefs.setIntPref("network.proxy.type", 2); + prefs.setCharPref("network.proxy.autoconfig_url", pac); + pps.asyncResolve(channel, 0, new TestResolveCallback("http", run_pac2_test)); +} + +function run_pac2_test() { + var pac = + "data:text/plain," + + "function FindProxyForURL(url, host) {" + + ' return "HTTPS foopy:8080; DIRECT";' + + "}"; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + // Configure PAC + originalTLSProxy = prefs.getBoolPref("network.proxy.proxy_over_tls"); + + prefs.setCharPref("network.proxy.autoconfig_url", pac); + prefs.setBoolPref("network.proxy.proxy_over_tls", true); + + pps.asyncResolve(channel, 0, new TestResolveCallback("https", run_pac3_test)); +} + +function run_pac3_test() { + var pac = + "data:text/plain," + + "function FindProxyForURL(url, host) {" + + ' return "HTTPS foopy:8080; DIRECT";' + + "}"; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + // Configure PAC + prefs.setCharPref("network.proxy.autoconfig_url", pac); + prefs.setBoolPref("network.proxy.proxy_over_tls", false); + + pps.asyncResolve(channel, 0, new TestResolveCallback(null, run_pac4_test)); +} + +function run_pac4_test() { + // Bug 1251332 + let wRange = [ + ["SUN", "MON", "SAT", "MON"], // for Sun + ["SUN", "TUE", "SAT", "TUE"], // for Mon + ["MON", "WED", "SAT", "WED"], // for Tue + ["TUE", "THU", "SAT", "THU"], // for Wed + ["WED", "FRI", "WED", "SUN"], // for Thu + ["THU", "SAT", "THU", "SUN"], // for Fri + ["FRI", "SAT", "FRI", "SUN"], // for Sat + ]; + let today = new Date().getDay(); + var pac = + "data:text/plain," + + "function FindProxyForURL(url, host) {" + + ' if (weekdayRange("' + + wRange[today][0] + + '", "' + + wRange[today][1] + + '") &&' + + ' weekdayRange("' + + wRange[today][2] + + '", "' + + wRange[today][3] + + '")) {' + + ' return "PROXY foopy:8080; DIRECT";' + + " }" + + "}"; + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + // Configure PAC + + prefs.setIntPref("network.proxy.type", 2); + prefs.setCharPref("network.proxy.autoconfig_url", pac); + pps.asyncResolve( + channel, + 0, + new TestResolveCallback("http", run_utf8_pac_test) + ); +} + +function run_utf8_pac_test() { + var pac = + "data:text/plain;charset=UTF-8," + + "function FindProxyForURL(url, host) {" + + " /*" + + " U+00A9 COPYRIGHT SIGN: %C2%A9," + + " U+0B87 TAMIL LETTER I: %E0%AE%87," + + " U+10398 UGARITIC LETTER THANNA: %F0%90%8E%98 " + + " */" + + ' var multiBytes = "%C2%A9 %E0%AE%87 %F0%90%8E%98"; ' + + " /* 6 UTF-16 units above if PAC script run as UTF-8; 11 units if run as Latin-1 */ " + + " return multiBytes.length === 6 " + + ' ? "PROXY foopy:8080; DIRECT" ' + + ' : "PROXY epicfail-utf8:12345; DIRECT";' + + "}"; + + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + + // Configure PAC + prefs.setIntPref("network.proxy.type", 2); + prefs.setCharPref("network.proxy.autoconfig_url", pac); + + pps.asyncResolve( + channel, + 0, + new TestResolveCallback("http", run_latin1_pac_test) + ); +} + +function run_latin1_pac_test() { + var pac = + "data:text/plain," + + "function FindProxyForURL(url, host) {" + + " /* A too-long encoding of U+0000, so not valid UTF-8 */ " + + ' var multiBytes = "%C0%80"; ' + + " /* 2 UTF-16 units because interpreted as Latin-1 */ " + + " return multiBytes.length === 2 " + + ' ? "PROXY foopy:8080; DIRECT" ' + + ' : "PROXY epicfail-latin1:12345; DIRECT";' + + "}"; + + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + + // Configure PAC + prefs.setIntPref("network.proxy.type", 2); + prefs.setCharPref("network.proxy.autoconfig_url", pac); + + pps.asyncResolve( + channel, + 0, + new TestResolveCallback("http", finish_pac_test) + ); +} + +function finish_pac_test() { + prefs.setBoolPref("network.proxy.proxy_over_tls", originalTLSProxy); + run_pac_cancel_test(); +} + +function TestResolveCancelationCallback() {} +TestResolveCancelationCallback.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyCallback"]), + + onProxyAvailable: function TestResolveCancelationCallback_onProxyAvailable( + req, + channel, + pi, + status + ) { + dump("*** channelURI=" + channel.URI.spec + ", status=" + status + "\n"); + + Assert.notEqual(req, null); + Assert.notEqual(channel, null); + Assert.equal(status, Cr.NS_ERROR_ABORT); + Assert.equal(pi, null); + + prefs.setCharPref("network.proxy.autoconfig_url", ""); + prefs.setIntPref("network.proxy.type", 0); + + run_proxy_host_filters_test(); + }, +}; + +function run_pac_cancel_test() { + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + // Configure PAC + var pac = + "data:text/plain," + + "function FindProxyForURL(url, host) {" + + ' return "PROXY foopy:8080; DIRECT";' + + "}"; + prefs.setIntPref("network.proxy.type", 2); + prefs.setCharPref("network.proxy.autoconfig_url", pac); + + var req = pps.asyncResolve(channel, 0, new TestResolveCancelationCallback()); + req.cancel(Cr.NS_ERROR_ABORT); +} + +var hostList; +var hostIDX; +var bShouldBeFiltered; +var hostNextFX; + +function check_host_filters(hl, shouldBe, nextFX) { + hostList = hl; + hostIDX = 0; + bShouldBeFiltered = shouldBe; + hostNextFX = nextFX; + + if (hostList.length > hostIDX) { + check_host_filter(hostIDX); + } +} + +function check_host_filters_cb() { + hostIDX++; + if (hostList.length > hostIDX) { + check_host_filter(hostIDX); + } else { + hostNextFX(); + } +} + +function check_host_filter(i) { + dump( + "*** uri=" + hostList[i] + " bShouldBeFiltered=" + bShouldBeFiltered + "\n" + ); + var channel = NetUtil.newChannel({ + uri: hostList[i], + loadUsingSystemPrincipal: true, + }); + var cb = new resolveCallback(); + cb.nextFunction = host_filter_cb; + pps.asyncResolve(channel, 0, cb); +} + +function host_filter_cb(proxy) { + if (bShouldBeFiltered) { + Assert.equal(proxy, null); + } else { + Assert.notEqual(proxy, null); + // Just to be sure, let's check that the proxy is correct + // - this should match the proxy setup in the calling function + check_proxy(proxy, "http", "foopy", 8080, 0, -1, false); + } + check_host_filters_cb(); +} + +// Verify that hists in the host filter list are not proxied +// refers to "network.proxy.no_proxies_on" + +var uriStrUseProxyList; +var hostFilterList; +var uriStrFilterList; + +function run_proxy_host_filters_test() { + // Get prefs object from DOM + // Setup a basic HTTP proxy configuration + // - pps.resolve() needs this to return proxy info for non-filtered hosts + prefs.setIntPref("network.proxy.type", 1); + prefs.setCharPref("network.proxy.http", "foopy"); + prefs.setIntPref("network.proxy.http_port", 8080); + + // Setup host filter list string for "no_proxies_on" + hostFilterList = + "www.mozilla.org, www.google.com, www.apple.com, " + + ".domain, .domain2.org"; + prefs.setCharPref("network.proxy.no_proxies_on", hostFilterList); + Assert.equal( + prefs.getCharPref("network.proxy.no_proxies_on"), + hostFilterList + ); + + // Check the hosts that should be filtered out + uriStrFilterList = [ + "http://www.mozilla.org/", + "http://www.google.com/", + "http://www.apple.com/", + "http://somehost.domain/", + "http://someotherhost.domain/", + "http://somehost.domain2.org/", + "http://somehost.subdomain.domain2.org/", + ]; + check_host_filters(uriStrFilterList, true, host_filters_1); +} + +function host_filters_1() { + // Check the hosts that should be proxied + uriStrUseProxyList = [ + "http://www.mozilla.com/", + "http://mail.google.com/", + "http://somehost.domain.co.uk/", + "http://somelocalhost/", + ]; + check_host_filters(uriStrUseProxyList, false, host_filters_2); +} + +function host_filters_2() { + // Set no_proxies_on to include local hosts + prefs.setCharPref( + "network.proxy.no_proxies_on", + hostFilterList + ", <local>" + ); + Assert.equal( + prefs.getCharPref("network.proxy.no_proxies_on"), + hostFilterList + ", <local>" + ); + // Amend lists - move local domain to filtered list + uriStrFilterList.push(uriStrUseProxyList.pop()); + check_host_filters(uriStrFilterList, true, host_filters_3); +} + +function host_filters_3() { + check_host_filters(uriStrUseProxyList, false, host_filters_4); +} + +function host_filters_4() { + // Cleanup + prefs.setCharPref("network.proxy.no_proxies_on", ""); + Assert.equal(prefs.getCharPref("network.proxy.no_proxies_on"), ""); + + run_myipaddress_test(); +} + +function run_myipaddress_test() { + // This test makes sure myIpAddress() comes up with some valid + // IP address other than localhost. The DUT must be configured with + // an Internet route for this to work - though no Internet traffic + // should be created. + + var pac = + "data:text/plain," + + "var pacUseMultihomedDNS = true;\n" + + "function FindProxyForURL(url, host) {" + + ' return "PROXY " + myIpAddress() + ":1234";' + + "}"; + + // no traffic to this IP is ever sent, it is just a public IP that + // does not require DNS to determine a route. + var channel = NetUtil.newChannel({ + uri: "http://192.0.43.10/", + loadUsingSystemPrincipal: true, + }); + prefs.setIntPref("network.proxy.type", 2); + prefs.setCharPref("network.proxy.autoconfig_url", pac); + + var cb = new resolveCallback(); + cb.nextFunction = myipaddress_callback; + pps.asyncResolve(channel, 0, cb); +} + +function myipaddress_callback(pi) { + Assert.notEqual(pi, null); + Assert.equal(pi.type, "http"); + Assert.equal(pi.port, 1234); + + // make sure we didn't return localhost + Assert.notEqual(pi.host, null); + Assert.notEqual(pi.host, "127.0.0.1"); + Assert.notEqual(pi.host, "::1"); + + run_myipaddress_test_2(); +} + +function run_myipaddress_test_2() { + // test that myIPAddress() can be used outside of the scope of + // FindProxyForURL(). bug 829646. + + var pac = + "data:text/plain," + + "var pacUseMultihomedDNS = true;\n" + + "var myaddr = myIpAddress(); " + + "function FindProxyForURL(url, host) {" + + ' return "PROXY " + myaddr + ":5678";' + + "}"; + + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + prefs.setIntPref("network.proxy.type", 2); + prefs.setCharPref("network.proxy.autoconfig_url", pac); + + var cb = new resolveCallback(); + cb.nextFunction = myipaddress2_callback; + pps.asyncResolve(channel, 0, cb); +} + +function myipaddress2_callback(pi) { + Assert.notEqual(pi, null); + Assert.equal(pi.type, "http"); + Assert.equal(pi.port, 5678); + + // make sure we didn't return localhost + Assert.notEqual(pi.host, null); + Assert.notEqual(pi.host, "127.0.0.1"); + Assert.notEqual(pi.host, "::1"); + + run_failed_script_test(); +} + +function run_failed_script_test() { + // test to make sure we go direct with invalid PAC + // eslint-disable-next-line no-useless-concat + var pac = "data:text/plain," + "\nfor(;\n"; + + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + prefs.setIntPref("network.proxy.type", 2); + prefs.setCharPref("network.proxy.autoconfig_url", pac); + + var cb = new resolveCallback(); + cb.nextFunction = failed_script_callback; + pps.asyncResolve(channel, 0, cb); +} + +var directFilter; +const TEST_URI = "http://127.0.0.1:7247/"; + +function failed_script_callback(pi) { + // we should go direct + Assert.equal(pi, null); + + // test that we honor filters when configured to go direct + prefs.setIntPref("network.proxy.type", 0); + directFilter = new TestFilter("http", "127.0.0.1", 7246, 0, 0); + pps.registerFilter(directFilter, 10); + + // test that on-modify-request contains the proxy info too + var obs = Cc["@mozilla.org/observer-service;1"].getService(); + obs = obs.QueryInterface(Ci.nsIObserverService); + obs.addObserver(directFilterListener, "http-on-modify-request"); + + var ssm = Services.scriptSecurityManager; + let uri = TEST_URI; + var chan = NetUtil.newChannel({ + uri, + loadingPrincipal: ssm.createContentPrincipal(Services.io.newURI(uri), {}), + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + chan.asyncOpen(directFilterListener); +} + +var directFilterListener = { + onModifyRequestCalled: false, + + onStartRequest: function test_onStart(request) {}, + onDataAvailable: function test_OnData() {}, + + onStopRequest: function test_onStop(request, status) { + // check on the PI from the channel itself + request.QueryInterface(Ci.nsIProxiedChannel); + check_proxy(request.proxyInfo, "http", "127.0.0.1", 7246, 0, 0, false); + pps.unregisterFilter(directFilter); + + // check on the PI from on-modify-request + Assert.ok(this.onModifyRequestCalled); + var obs = Cc["@mozilla.org/observer-service;1"].getService(); + obs = obs.QueryInterface(Ci.nsIObserverService); + obs.removeObserver(this, "http-on-modify-request"); + + run_isresolvable_test(); + }, + + observe(subject, topic, data) { + if ( + topic === "http-on-modify-request" && + subject instanceof Ci.nsIHttpChannel && + subject instanceof Ci.nsIProxiedChannel + ) { + info("check proxy in observe uri=" + subject.URI.spec); + if (subject.URI.spec != TEST_URI) { + return; + } + check_proxy(subject.proxyInfo, "http", "127.0.0.1", 7246, 0, 0, false); + this.onModifyRequestCalled = true; + } + }, +}; + +function run_isresolvable_test() { + // test a non resolvable host in the pac file + + var pac = + "data:text/plain," + + "function FindProxyForURL(url, host) {" + + ' if (isResolvable("nonexistant.lan.onion"))' + + ' return "DIRECT";' + + ' return "PROXY 127.0.0.1:1234";' + + "}"; + + var channel = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }); + prefs.setIntPref("network.proxy.type", 2); + prefs.setCharPref("network.proxy.autoconfig_url", pac); + + var cb = new resolveCallback(); + cb.nextFunction = isresolvable_callback; + pps.asyncResolve(channel, 0, cb); +} + +function isresolvable_callback(pi) { + Assert.notEqual(pi, null); + Assert.equal(pi.type, "http"); + Assert.equal(pi.port, 1234); + Assert.equal(pi.host, "127.0.0.1"); + + run_localhost_pac(); +} + +function run_localhost_pac() { + // test localhost in the pac file + + var pac = + "data:text/plain," + + "function FindProxyForURL(url, host) {" + + ' return "PROXY totallycrazy:1234";' + + "}"; + + // Use default filter list string for "no_proxies_on" ("localhost, 127.0.0.1") + prefs.clearUserPref("network.proxy.no_proxies_on"); + var channel = NetUtil.newChannel({ + uri: "http://localhost/", + loadUsingSystemPrincipal: true, + }); + prefs.setIntPref("network.proxy.type", 2); + prefs.setCharPref("network.proxy.autoconfig_url", pac); + + var cb = new resolveCallback(); + cb.nextFunction = localhost_callback; + pps.asyncResolve(channel, 0, cb); +} + +function localhost_callback(pi) { + Assert.equal(pi, null); // no proxy! + + prefs.setIntPref("network.proxy.type", 0); + + if (mozinfo.socketprocess_networking && again) { + info("run test again"); + again = false; + cleanUp(); + prefs.setBoolPref("network.proxy.parse_pac_on_socket_process", true); + run_filter_test(); + } else { + cleanUp(); + do_test_finished(); + } +} + +function cleanUp() { + prefs.clearUserPref("network.proxy.type"); + prefs.clearUserPref("network.proxy.http"); + prefs.clearUserPref("network.proxy.http_port"); + prefs.clearUserPref("network.proxy.socks"); + prefs.clearUserPref("network.proxy.socks_port"); + prefs.clearUserPref("network.proxy.autoconfig_url"); + prefs.clearUserPref("network.proxy.proxy_over_tls"); + prefs.clearUserPref("network.proxy.no_proxies_on"); + prefs.clearUserPref("network.proxy.parse_pac_on_socket_process"); +} + +function run_test() { + register_test_protocol_handler(); + + prefs.setBoolPref("network.proxy.parse_pac_on_socket_process", false); + // start of asynchronous test chain + run_filter_test(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_proxy-failover_canceled.js b/netwerk/test/unit/test_proxy-failover_canceled.js new file mode 100644 index 0000000000..528487a343 --- /dev/null +++ b/netwerk/test/unit/test_proxy-failover_canceled.js @@ -0,0 +1,55 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); +} + +const responseBody = "response body"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function finish_test(request, buffer) { + Assert.equal(buffer, ""); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + // we want to cancel the failover proxy engage, so, do not allow + // redirects from now. + + var nc = new ChannelEventSink(); + nc._flags = ES_ABORT_REDIRECT; + + var prefs = Services.prefs.getBranch("network.proxy."); + prefs.setIntPref("type", 2); + prefs.setCharPref("no_proxies_on", "nothing"); + prefs.setBoolPref("allow_hijacking_localhost", true); + prefs.setCharPref( + "autoconfig_url", + "data:text/plain," + + "function FindProxyForURL(url, host) {return 'PROXY a_non_existent_domain_x7x6c572v:80; PROXY localhost:" + + httpServer.identity.primaryPort + + "';}" + ); + + var chan = make_channel( + "http://localhost:" + httpServer.identity.primaryPort + "/content" + ); + chan.notificationCallbacks = nc; + chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_proxy-failover_passing.js b/netwerk/test/unit/test_proxy-failover_passing.js new file mode 100644 index 0000000000..520723bffd --- /dev/null +++ b/netwerk/test/unit/test_proxy-failover_passing.js @@ -0,0 +1,43 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function finish_test(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var prefs = Services.prefs.getBranch("network.proxy."); + prefs.setIntPref("type", 2); + prefs.setCharPref( + "autoconfig_url", + "data:text/plain," + + "function FindProxyForURL(url, host) {return 'PROXY a_non_existent_domain_x7x6c572v:80; PROXY localhost:" + + httpServer.identity.primaryPort + + "';}" + ); + + var chan = make_channel( + "http://localhost:" + httpServer.identity.primaryPort + "/content" + ); + chan.asyncOpen(new ChannelListener(finish_test, null)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_proxy-replace_canceled.js b/netwerk/test/unit/test_proxy-replace_canceled.js new file mode 100644 index 0000000000..5b9a65af3d --- /dev/null +++ b/netwerk/test/unit/test_proxy-replace_canceled.js @@ -0,0 +1,55 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); +} + +const responseBody = "response body"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function finish_test(request, buffer) { + Assert.equal(buffer, ""); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var prefs = Services.prefs.getBranch("network.proxy."); + prefs.setIntPref("type", 2); + prefs.setCharPref( + "autoconfig_url", + "data:text/plain," + + "function FindProxyForURL(url, host) {return 'PROXY localhost:" + + httpServer.identity.primaryPort + + "';}" + ); + + // this test assumed that a AsyncOnChannelRedirect query is made for + // each proxy failover or on the inital proxy only when PAC mode is used. + // Neither of those are documented anywhere that I can find and the latter + // hasn't been a useful property because it is PAC dependent and the type + // is generally unknown and OS driven. 769764 changed that to remove the + // internal redirect used to setup the initial proxy/channel as that isn't + // a redirect in any sense. + + var chan = make_channel( + "http://localhost:" + httpServer.identity.primaryPort + "/content" + ); + chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE)); + chan.cancel(Cr.NS_BINDING_ABORTED); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_proxy-replace_passing.js b/netwerk/test/unit/test_proxy-replace_passing.js new file mode 100644 index 0000000000..35074da327 --- /dev/null +++ b/netwerk/test/unit/test_proxy-replace_passing.js @@ -0,0 +1,43 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function finish_test(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var prefs = Services.prefs.getBranch("network.proxy."); + prefs.setIntPref("type", 2); + prefs.setCharPref( + "autoconfig_url", + "data:text/plain," + + "function FindProxyForURL(url, host) {return 'PROXY localhost:" + + httpServer.identity.primaryPort + + "';}" + ); + + var chan = make_channel( + "http://localhost:" + httpServer.identity.primaryPort + "/content" + ); + chan.asyncOpen(new ChannelListener(finish_test, null)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_proxy-slow-upload.js b/netwerk/test/unit/test_proxy-slow-upload.js new file mode 100644 index 0000000000..4bfbe56f10 --- /dev/null +++ b/netwerk/test/unit/test_proxy-slow-upload.js @@ -0,0 +1,105 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ + +const SIZE = 4096; +const CONTENT = "x".repeat(SIZE); + +add_task(async function test_slow_upload() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + let proxies = [ + NodeHTTPProxyServer, + NodeHTTPSProxyServer, + NodeHTTP2ProxyServer, + ]; + for (let p of proxies) { + let proxy = new p(); + await proxy.start(); + registerCleanupFunction(async () => { + await proxy.stop(); + }); + + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + info(`Testing ${p.name} with ${server.constructor.name}`); + await server.execute( + `global.server_name = "${server.constructor.name}";` + ); + await server.registerPathHandler("/test", (req, resp) => { + let content = ""; + req.on("data", data => { + global.data_count = (global.data_count || 0) + 1; + content += data; + }); + req.on("end", () => { + resp.writeHead(200); + resp.end(content); + }); + }); + + let sstream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + sstream.data = CONTENT; + + let mime = Cc[ + "@mozilla.org/network/mime-input-stream;1" + ].createInstance(Ci.nsIMIMEInputStream); + mime.addHeader("Content-Type", "multipart/form-data; boundary=zzzzz"); + mime.setData(sstream); + + let tq = Cc["@mozilla.org/network/throttlequeue;1"].createInstance( + Ci.nsIInputChannelThrottleQueue + ); + // Make sure the request takes more than one read. + tq.init(100 + SIZE / 2, 100 + SIZE / 2); + + let chan = NetUtil.newChannel({ + uri: `${server.origin()}/test`, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + + let tic = chan.QueryInterface(Ci.nsIThrottledInputChannel); + tic.throttleQueue = tq; + + chan + .QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(mime, "", mime.available()); + chan.requestMethod = "POST"; + + let { req, buff } = await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + (req, buff) => resolve({ req, buff }), + null, + CL_ALLOW_UNKNOWN_CL + ) + ); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + ok(buff == CONTENT, "Content must match"); + ok(!!req.QueryInterface(Ci.nsIProxiedChannel).proxyInfo); + greater( + await server.execute(`global.data_count`), + 1, + "Content should have been streamed to the server in several chunks" + ); + } + ); + await proxy.stop(); + } +}); diff --git a/netwerk/test/unit/test_proxy_cancel.js b/netwerk/test/unit/test_proxy_cancel.js new file mode 100644 index 0000000000..d891f3147c --- /dev/null +++ b/netwerk/test/unit/test_proxy_cancel.js @@ -0,0 +1,397 @@ +/* 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"; + +/* globals setTimeout */ + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +add_task(async function test_cancel_after_asyncOpen() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + let proxies = [ + NodeHTTPProxyServer, + NodeHTTPSProxyServer, + NodeHTTP2ProxyServer, + ]; + for (let p of proxies) { + let proxy = new p(); + await proxy.start(); + registerCleanupFunction(async () => { + await proxy.stop(); + }); + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + info(`Testing ${p.name} with ${server.constructor.name}`); + await server.execute( + `global.server_name = "${server.constructor.name}";` + ); + + await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200); + resp.end(global.server_name); + }); + + let chan = makeChan(`${server.origin()}/test`); + let openPromise = new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + (req, buff) => resolve({ req, buff }), + null, + CL_EXPECT_FAILURE + ) + ); + }); + chan.cancel(Cr.NS_ERROR_ABORT); + let { req } = await openPromise; + Assert.equal(req.status, Cr.NS_ERROR_ABORT); + } + ); + await proxy.stop(); + } +}); + +// const NS_NET_STATUS_CONNECTING_TO = 0x4b0007; +// const NS_NET_STATUS_CONNECTED_TO = 0x4b0004; +// const NS_NET_STATUS_SENDING_TO = 0x4b0005; +const NS_NET_STATUS_WAITING_FOR = 0x4b000a; // 2152398858 +const NS_NET_STATUS_RECEIVING_FROM = 0x4b0006; +// const NS_NET_STATUS_TLS_HANDSHAKE_STARTING = 0x4b000c; // 2152398860 +// const NS_NET_STATUS_TLS_HANDSHAKE_ENDED = 0x4b000d; // 2152398861 + +add_task(async function test_cancel_after_connect_http2proxy() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + // Set up a proxy for each server to make sure proxy state is clean + // for each test. + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); + registerCleanupFunction(async () => { + await proxy.stop(); + }); + await proxy.execute(` + global.session_counter = 0; + global.proxy.on("session", () => { + global.session_counter++; + }); + `); + + info(`Testing ${proxy.constructor.name} with ${server.constructor.name}`); + await server.execute( + `global.server_name = "${server.constructor.name}";` + ); + + await server.registerPathHandler("/test", (req, resp) => { + global.reqCount = (global.reqCount || 0) + 1; + resp.writeHead(200); + resp.end(global.server_name); + }); + + let chan = makeChan(`${server.origin()}/test`); + chan.notificationCallbacks = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIProgressEventSink", + ]), + + getInterface(iid) { + return this.QueryInterface(iid); + }, + + onProgress(request, progress, progressMax) {}, + onStatus(request, status, statusArg) { + info(`status = ${status}`); + // XXX(valentin): Is this the best status to be cancelling? + if (status == NS_NET_STATUS_WAITING_FOR) { + info("cancelling connected channel"); + chan.cancel(Cr.NS_ERROR_ABORT); + } + }, + }; + let openPromise = new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + (req, buff) => resolve({ req, buff }), + null, + CL_EXPECT_FAILURE + ) + ); + }); + let { req } = await openPromise; + Assert.equal(req.status, Cr.NS_ERROR_ABORT); + + // Since we're cancelling just after connect, we'd expect that no + // requests are actually registered. But because we're cancelling on the + // main thread, and the request is being performed on the socket thread, + // it might actually reach the server, especially in chaos test mode. + // Assert.equal( + // await server.execute(`global.reqCount || 0`), + // 0, + // `No requests should have been made at this point` + // ); + Assert.equal(await proxy.execute(`global.session_counter`), 1); + + chan = makeChan(`${server.origin()}/test`); + await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + // eslint-disable-next-line no-shadow + (req, buff) => resolve({ req, buff }), + null, + CL_ALLOW_UNKNOWN_CL + ) + ); + }); + + // Check that there's still only one session. + Assert.equal(await proxy.execute(`global.session_counter`), 1); + await proxy.stop(); + } + ); +}); + +add_task(async function test_cancel_after_sending_request() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + let proxies = [ + NodeHTTPProxyServer, + NodeHTTPSProxyServer, + NodeHTTP2ProxyServer, + ]; + for (let p of proxies) { + let proxy = new p(); + await proxy.start(); + registerCleanupFunction(async () => { + await proxy.stop(); + }); + + await proxy.execute(` + global.session_counter = 0; + global.proxy.on("session", () => { + global.session_counter++; + }); + `); + info(`Testing ${p.name} with ${server.constructor.name}`); + await server.execute( + `global.server_name = "${server.constructor.name}";` + ); + + await server.registerPathHandler("/test", (req, resp) => { + // Here we simmulate a slow response to give the test time to + // cancel the channel before receiving the response. + global.request_count = (global.request_count || 0) + 1; + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(() => { + resp.writeHead(200); + resp.end(global.server_name); + }, 2000); + }); + await server.registerPathHandler("/instant", (req, resp) => { + resp.writeHead(200); + resp.end(global.server_name); + }); + + // It seems proxy.on("session") only gets emitted after a full request. + // So we first load a simple request, then the long lasting request + // that we then cancel before it has the chance to complete. + let chan = makeChan(`${server.origin()}/instant`); + await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL) + ); + }); + + chan = makeChan(`${server.origin()}/test`); + let openPromise = new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + (req, buff) => resolve({ req, buff }), + null, + CL_EXPECT_FAILURE + ) + ); + }); + // XXX(valentin) This might be a little racy + await TestUtils.waitForCondition(async () => { + return (await server.execute("global.request_count")) > 0; + }); + + chan.cancel(Cr.NS_ERROR_ABORT); + + let { req } = await openPromise; + Assert.equal(req.status, Cr.NS_ERROR_ABORT); + + async function checkSessionCounter() { + if (p.name == "NodeHTTP2ProxyServer") { + Assert.equal(await proxy.execute(`global.session_counter`), 1); + } + } + + await checkSessionCounter(); + + chan = makeChan(`${server.origin()}/instant`); + await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + // eslint-disable-next-line no-shadow + (req, buff) => resolve({ req, buff }), + null, + CL_ALLOW_UNKNOWN_CL + ) + ); + }); + await checkSessionCounter(); + + await proxy.stop(); + } + } + ); +}); + +add_task(async function test_cancel_during_response() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + let proxies = [ + NodeHTTPProxyServer, + NodeHTTPSProxyServer, + NodeHTTP2ProxyServer, + ]; + for (let p of proxies) { + let proxy = new p(); + await proxy.start(); + registerCleanupFunction(async () => { + await proxy.stop(); + }); + + await proxy.execute(` + global.session_counter = 0; + global.proxy.on("session", () => { + global.session_counter++; + }); + `); + + info(`Testing ${p.name} with ${server.constructor.name}`); + await server.execute( + `global.server_name = "${server.constructor.name}";` + ); + + await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200); + resp.write("a".repeat(1000)); + // Here we send the response back in two chunks. + // The channel should be cancelled after the first one. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(() => { + resp.write("a".repeat(1000)); + resp.end(global.server_name); + }, 2000); + }); + await server.registerPathHandler("/instant", (req, resp) => { + resp.writeHead(200); + resp.end(global.server_name); + }); + + let chan = makeChan(`${server.origin()}/test`); + + chan.notificationCallbacks = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIProgressEventSink", + ]), + + getInterface(iid) { + return this.QueryInterface(iid); + }, + + onProgress(request, progress, progressMax) { + info(`progress: ${progress}/${progressMax}`); + // Check that we never get more than 1000 bytes. + Assert.equal(progress, 1000); + }, + onStatus(request, status, statusArg) { + if (status == NS_NET_STATUS_RECEIVING_FROM) { + info("cancelling when receiving request"); + chan.cancel(Cr.NS_ERROR_ABORT); + } + }, + }; + + let openPromise = new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + (req, buff) => resolve({ req, buff }), + null, + CL_EXPECT_FAILURE + ) + ); + }); + + let { req } = await openPromise; + Assert.equal(req.status, Cr.NS_ERROR_ABORT); + + async function checkSessionCounter() { + if (p.name == "NodeHTTP2ProxyServer") { + Assert.equal(await proxy.execute(`global.session_counter`), 1); + } + } + + await checkSessionCounter(); + + chan = makeChan(`${server.origin()}/instant`); + await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + // eslint-disable-next-line no-shadow + (req, buff) => resolve({ req, buff }), + null, + CL_ALLOW_UNKNOWN_CL + ) + ); + }); + + await checkSessionCounter(); + + await proxy.stop(); + } + } + ); +}); diff --git a/netwerk/test/unit/test_proxy_pac.js b/netwerk/test/unit/test_proxy_pac.js new file mode 100644 index 0000000000..343ad771fb --- /dev/null +++ b/netwerk/test/unit/test_proxy_pac.js @@ -0,0 +1,126 @@ +// These are globlas defined for proxy servers, in ProxyAutoConfig.cpp. See +// PACGlobalFunctions +/* globals dnsResolve, alert */ + +"use strict"; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); +const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); + +class ConsoleListener { + messages = []; + observe(message) { + // Ignore unexpected messages. + if (!(message instanceof Ci.nsIConsoleMessage)) { + return; + } + if (!message.message.includes("PAC")) { + return; + } + + this.messages.push(message.message); + } + + register() { + Services.console.registerListener(this); + } + + unregister() { + Services.console.unregisterListener(this); + } + + clear() { + this.messages = []; + } +} + +async function configurePac(fn) { + let pacURI = `data:application/x-ns-proxy-autoconfig;charset=utf-8,${encodeURIComponent( + fn.toString() + )}`; + Services.prefs.setIntPref("network.proxy.type", 2); + Services.prefs.setStringPref("network.proxy.autoconfig_url", pacURI); + + // Do a request so the PAC gets loaded + let chan = NetUtil.newChannel({ + uri: `http://localhost:1234/`, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + await new Promise(resolve => + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)) + ); + + await TestUtils.waitForCondition( + () => + !!consoleListener.messages.filter( + e => e.includes("PAC file installed from"), + 0 + ).length, + "Wait for PAC file to be installed." + ); + consoleListener.clear(); +} + +let consoleListener = null; +function setup() { + Services.prefs.setIntPref("network.proxy.type", 2); + Services.prefs.setStringPref("network.proxy.autoconfig_url", ""); + consoleListener = new ConsoleListener(); + consoleListener.register(); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.proxy.type"); + Services.prefs.clearUserPref("network.proxy.autoconfig_url"); + if (consoleListener) { + consoleListener.unregister(); + consoleListener = null; + } + }); +} +setup(); + +// This method checks that calling dnsResult(null) does not result in +// resolving the DNS name "null" +add_task(async function test_bug1724345() { + consoleListener.clear(); + // isInNet is defined by ascii_pac_utils.js which is included for proxies. + /* globals isInNet */ + let pac = function FindProxyForURL(url, host) { + alert(`PAC resolving: ${host}`); + let destIP = dnsResolve(host); + alert(`PAC result: ${destIP}`); + alert( + `PAC isInNet: ${host} ${destIP} ${isInNet( + destIP, + "127.0.0.0", + "255.0.0.0" + )}` + ); + return "DIRECT"; + }; + + await configurePac(pac); + + override.clearOverrides(); + override.addIPOverride("example.org", "N/A"); + override.addIPOverride("null", "127.0.0.1"); + Services.dns.clearCache(true); + + let chan = NetUtil.newChannel({ + uri: `http://example.org:1234/`, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + await new Promise(resolve => + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)) + ); + ok( + !!consoleListener.messages.filter(e => + e.includes("PAC isInNet: example.org null false") + ).length, + `should have proper result ${consoleListener.messages}` + ); +}); diff --git a/netwerk/test/unit/test_proxyconnect.js b/netwerk/test/unit/test_proxyconnect.js new file mode 100644 index 0000000000..a22bb25b31 --- /dev/null +++ b/netwerk/test/unit/test_proxyconnect.js @@ -0,0 +1,360 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +// test_connectonly tests happy path of proxy connect +// 1. CONNECT to localhost:socketserver_port +// 2. Write 200 Connection established +// 3. Write data to the tunnel (and read server-side) +// 4. Read data from the tunnel (and write server-side) +// 5. done +// test_connectonly_noproxy tests an http channel with only connect set but +// no proxy configured. +// 1. OnTransportAvailable callback NOT called (checked in step 2) +// 2. StopRequest callback called +// 3. done +// test_connectonly_nonhttp tests an http channel with only connect set with a +// non-http proxy. +// 1. OnTransportAvailable callback NOT called (checked in step 2) +// 2. StopRequest callback called +// 3. done + +// -1 then initialized with an actual port from the serversocket +"use strict"; + +var socketserver_port = -1; + +const CC = Components.Constructor; +const ServerSocket = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init" +); +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +const BinaryOutputStream = CC( + "@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream" +); + +const STATE_NONE = 0; +const STATE_READ_CONNECT_REQUEST = 1; +const STATE_WRITE_CONNECTION_ESTABLISHED = 2; +const STATE_CHECK_WRITE = 3; // write to the tunnel +const STATE_CHECK_WRITE_READ = 4; // wrote to the tunnel, check connection data +const STATE_CHECK_READ = 5; // read from the tunnel +const STATE_CHECK_READ_WROTE = 6; // wrote to connection, check tunnel data +const STATE_COMPLETED = 100; + +const CONNECT_RESPONSE_STRING = "HTTP/1.1 200 Connection established\r\n\r\n"; +const CHECK_WRITE_STRING = "hello"; +const CHECK_READ_STRING = "world"; +const ALPN = "webrtc"; + +var connectRequest = ""; +var checkWriteData = ""; +var checkReadData = ""; + +var threadManager; +var socket; +var streamIn; +var streamOut; +var accepted = false; +var acceptedSocket; +var state = STATE_NONE; +var transportAvailable = false; +var proxiedChannel; +var listener = { + expectedCode: -1, // uninitialized + + onStartRequest: function test_onStartR(request) {}, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + if (state === STATE_COMPLETED) { + Assert.equal(transportAvailable, false, "transport available not called"); + Assert.equal(status, 0x80004005, "error code matches"); + Assert.equal(proxiedChannel.httpProxyConnectResponseCode, 200); + nextTest(); + return; + } + + Assert.equal(accepted, true, "socket accepted"); + accepted = false; + }, +}; + +var upgradeListener = { + onTransportAvailable: (transport, socketIn, socketOut) => { + if (!transport || !socketIn || !socketOut) { + do_throw("on transport available failed"); + } + + if (state !== STATE_CHECK_WRITE) { + do_throw("bad state"); + } + + transportAvailable = true; + + socketIn.asyncWait(connectHandler, 0, 0, threadManager.mainThread); + socketOut.asyncWait(connectHandler, 0, 0, threadManager.mainThread); + }, + QueryInterface: ChromeUtils.generateQI(["nsIHttpUpgradeListener"]), +}; + +var connectHandler = { + onInputStreamReady: input => { + try { + const bis = new BinaryInputStream(input); + var data = bis.readByteArray(input.available()); + + dataAvailable(data); + + if (state !== STATE_COMPLETED) { + input.asyncWait(connectHandler, 0, 0, threadManager.mainThread); + } + } catch (e) { + do_throw(e); + } + }, + onOutputStreamReady: output => { + writeData(output); + }, + QueryInterface: iid => { + if ( + iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIInputStreamCallback) || + iid.equals(Ci.nsIOutputStreamCallback) + ) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, +}; + +function dataAvailable(data) { + switch (state) { + case STATE_READ_CONNECT_REQUEST: + connectRequest += String.fromCharCode.apply(String, data); + const headerEnding = connectRequest.indexOf("\r\n\r\n"); + const alpnHeaderIndex = connectRequest.indexOf(`ALPN: ${ALPN}`); + + if (headerEnding != -1) { + const requestLine = `CONNECT localhost:${socketserver_port} HTTP/1.1`; + Assert.equal(connectRequest.indexOf(requestLine), 0, "connect request"); + Assert.equal(headerEnding, connectRequest.length - 4, "req head only"); + Assert.notEqual(alpnHeaderIndex, -1, "alpn header found"); + + state = STATE_WRITE_CONNECTION_ESTABLISHED; + streamOut.asyncWait(connectHandler, 0, 0, threadManager.mainThread); + } + + break; + case STATE_CHECK_WRITE_READ: + checkWriteData += String.fromCharCode.apply(String, data); + + if (checkWriteData.length >= CHECK_WRITE_STRING.length) { + Assert.equal(checkWriteData, CHECK_WRITE_STRING, "correct write data"); + + state = STATE_CHECK_READ; + streamOut.asyncWait(connectHandler, 0, 0, threadManager.mainThread); + } + + break; + case STATE_CHECK_READ_WROTE: + checkReadData += String.fromCharCode.apply(String, data); + + if (checkReadData.length >= CHECK_READ_STRING.length) { + Assert.equal(checkReadData, CHECK_READ_STRING, "correct read data"); + + state = STATE_COMPLETED; + + streamIn.asyncWait(null, 0, 0, null); + acceptedSocket.close(0); + + nextTest(); + } + + break; + default: + do_throw("bad state: " + state); + } +} + +function writeData(output) { + let bos = new BinaryOutputStream(output); + + switch (state) { + case STATE_WRITE_CONNECTION_ESTABLISHED: + bos.write(CONNECT_RESPONSE_STRING, CONNECT_RESPONSE_STRING.length); + state = STATE_CHECK_WRITE; + break; + case STATE_CHECK_READ: + bos.write(CHECK_READ_STRING, CHECK_READ_STRING.length); + state = STATE_CHECK_READ_WROTE; + break; + case STATE_CHECK_WRITE: + bos.write(CHECK_WRITE_STRING, CHECK_WRITE_STRING.length); + state = STATE_CHECK_WRITE_READ; + break; + default: + do_throw("bad state: " + state); + } +} + +function makeChan(url) { + if (!url) { + url = "https://localhost:" + socketserver_port + "/"; + } + + var flags = + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL | + Ci.nsILoadInfo.SEC_DONT_FOLLOW_REDIRECTS | + Ci.nsILoadInfo.SEC_COOKIES_OMIT; + + var chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + securityFlags: flags, + }); + chan = chan.QueryInterface(Ci.nsIHttpChannel); + + var internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.HTTPUpgrade(ALPN, upgradeListener); + internal.setConnectOnly(); + + return chan; +} + +function socketAccepted(socket, transport) { + accepted = true; + + // copied from httpd.js + const SEGMENT_SIZE = 8192; + const SEGMENT_COUNT = 1024; + + switch (state) { + case STATE_NONE: + state = STATE_READ_CONNECT_REQUEST; + break; + default: + return; + } + + acceptedSocket = transport; + + try { + streamIn = transport + .openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT) + .QueryInterface(Ci.nsIAsyncInputStream); + streamOut = transport + .openOutputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncOutputStream); + + streamIn.asyncWait(connectHandler, 0, 0, threadManager.mainThread); + } catch (e) { + transport.close(Cr.NS_BINDING_ABORTED); + do_throw(e); + } +} + +function stopListening(socket, status) { + if (tests && tests.length !== 0 && do_throw) { + do_throw("should never stop"); + } +} + +function createProxy() { + try { + threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + + socket = new ServerSocket(-1, true, 1); + socketserver_port = socket.port; + + socket.asyncListen({ + onSocketAccepted: socketAccepted, + onStopListening: stopListening, + }); + } catch (e) { + do_throw(e); + } +} + +function test_connectonly() { + Services.prefs.setCharPref("network.proxy.ssl", "localhost"); + Services.prefs.setIntPref("network.proxy.ssl_port", socketserver_port); + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + Services.prefs.setIntPref("network.proxy.type", 1); + + var chan = makeChan(); + proxiedChannel = chan.QueryInterface(Ci.nsIProxiedChannel); + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_connectonly_noproxy() { + clearPrefs(); + var chan = makeChan(); + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_connectonly_nonhttp() { + clearPrefs(); + + Services.prefs.setCharPref("network.proxy.socks", "localhost"); + Services.prefs.setIntPref("network.proxy.socks_port", socketserver_port); + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + Services.prefs.setIntPref("network.proxy.type", 1); + + var chan = makeChan(); + chan.asyncOpen(listener); + + do_test_pending(); +} + +function nextTest() { + transportAvailable = false; + + if (!tests.length) { + do_test_finished(); + return; + } + + tests.shift()(); + do_test_finished(); +} + +var tests = [ + test_connectonly, + test_connectonly_noproxy, + test_connectonly_nonhttp, +]; + +function clearPrefs() { + Services.prefs.clearUserPref("network.proxy.ssl"); + Services.prefs.clearUserPref("network.proxy.ssl_port"); + Services.prefs.clearUserPref("network.proxy.socks"); + Services.prefs.clearUserPref("network.proxy.socks_port"); + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + Services.prefs.clearUserPref("network.proxy.type"); +} + +function run_test() { + createProxy(); + + registerCleanupFunction(clearPrefs); + + nextTest(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_psl.js b/netwerk/test/unit/test_psl.js new file mode 100644 index 0000000000..d9c0c2965d --- /dev/null +++ b/netwerk/test/unit/test_psl.js @@ -0,0 +1,39 @@ +"use strict"; + +var idna = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService +); + +function run_test() { + var file = do_get_file("data/test_psl.txt"); + var uri = Services.io.newFileURI(file); + var srvScope = {}; + Services.scriptloader.loadSubScript(uri.spec, srvScope); +} + +// Exported to the loaded script +/* exported checkPublicSuffix */ +function checkPublicSuffix(host, expectedSuffix) { + var actualSuffix = null; + try { + actualSuffix = Services.eTLD.getBaseDomainFromHost(host); + } catch (e) { + if ( + e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS && + e.result != Cr.NS_ERROR_ILLEGAL_VALUE + ) { + throw e; + } + } + // The EffectiveTLDService always gives back punycoded labels. + // The test suite wants to get back what it put in. + if ( + actualSuffix !== null && + expectedSuffix !== null && + /(^|\.)xn--/.test(actualSuffix) && + !/(^|\.)xn--/.test(expectedSuffix) + ) { + actualSuffix = idna.convertACEtoUTF8(actualSuffix); + } + Assert.equal(actualSuffix, expectedSuffix); +} diff --git a/netwerk/test/unit/test_race_cache_with_network.js b/netwerk/test/unit/test_race_cache_with_network.js new file mode 100644 index 0000000000..9bb6939558 --- /dev/null +++ b/netwerk/test/unit/test_race_cache_with_network.js @@ -0,0 +1,263 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +httpserver.start(-1); +const PORT = httpserver.identity.primaryPort; + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +let gResponseBody = "blahblah"; +let g200Counter = 0; +let g304Counter = 0; +function test_handler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("ETag", "test-etag1"); + + let etag; + try { + etag = metadata.getHeader("If-None-Match"); + } catch (ex) { + etag = ""; + } + + if (etag == "test-etag1") { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + g304Counter++; + } else { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(gResponseBody, gResponseBody.length); + g200Counter++; + } +} + +function cached_handler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "max-age=3600"); + response.setHeader("ETag", "test-etag1"); + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(gResponseBody, gResponseBody.length); + + g200Counter++; +} + +let gResponseCounter = 0; +let gIsFromCache = 0; +function checkContent(request, buffer, context, isFromCache) { + Assert.equal(buffer, gResponseBody); + info( + "isRacing: " + + request.QueryInterface(Ci.nsICacheInfoChannel).isRacing() + + "\n" + ); + gResponseCounter++; + if (isFromCache) { + gIsFromCache++; + } + executeSoon(() => { + testGenerator.next(); + }); +} + +function run_test() { + do_get_profile(); + // In this test, we manually use |TriggerNetwork| to prove we could send + // net and cache reqeust simultaneously. Therefore we should disable + // racing in the HttpChannel first. + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + httpserver.registerPathHandler("/rcwn", test_handler); + httpserver.registerPathHandler("/rcwn_cached", cached_handler); + testGenerator.next(); + do_test_pending(); +} + +let testGenerator = testSteps(); +function* testSteps() { + /* + * In this test, we have a relatively low timeout of 200ms and an assertion that + * the timer works properly by checking that the time was greater than 200ms. + * With a timer precision of 100ms (for example) we will clamp downwards to 200 + * and cause the assertion to fail. To resolve this, we hardcode a precision of + * 20ms. + */ + Services.prefs.setBoolPref("privacy.reduceTimerPrecision", true); + Services.prefs.setIntPref( + "privacy.resistFingerprinting.reduceTimerPrecision.microseconds", + 20000 + ); + + registerCleanupFunction(function () { + Services.prefs.clearUserPref("privacy.reduceTimerPrecision"); + Services.prefs.clearUserPref( + "privacy.resistFingerprinting.reduceTimerPrecision.microseconds" + ); + }); + + // Initial request. Stores the response in the cache. + let channel = make_channel("http://localhost:" + PORT + "/rcwn"); + channel.asyncOpen(new ChannelListener(checkContent, null)); + yield undefined; + equal(gResponseCounter, 1); + equal(g200Counter, 1, "check number of 200 responses"); + equal(g304Counter, 0, "check number of 304 responses"); + + // Checks that response is returned from the cache, after a 304 response. + channel = make_channel("http://localhost:" + PORT + "/rcwn"); + channel.asyncOpen(new ChannelListener(checkContent, null)); + yield undefined; + equal(gResponseCounter, 2); + equal(g200Counter, 1, "check number of 200 responses"); + equal(g304Counter, 1, "check number of 304 responses"); + + // Checks that delaying the response from the cache works. + channel = make_channel("http://localhost:" + PORT + "/rcwn"); + channel + .QueryInterface(Ci.nsIRaceCacheWithNetwork) + .test_delayCacheEntryOpeningBy(200); + let startTime = Date.now(); + channel.asyncOpen(new ChannelListener(checkContent, null)); + yield undefined; + greaterOrEqual( + Date.now() - startTime, + 200, + "Check that timer works properly" + ); + equal(gResponseCounter, 3); + equal(g200Counter, 1, "check number of 200 responses"); + equal(g304Counter, 2, "check number of 304 responses"); + + // Checks that we can trigger the cache open immediately, even if the cache delay is set very high. + channel = make_channel("http://localhost:" + PORT + "/rcwn"); + channel + .QueryInterface(Ci.nsIRaceCacheWithNetwork) + .test_delayCacheEntryOpeningBy(100000); + channel.asyncOpen(new ChannelListener(checkContent, null)); + do_timeout(50, function () { + channel + .QueryInterface(Ci.nsIRaceCacheWithNetwork) + .test_triggerDelayedOpenCacheEntry(); + }); + yield undefined; + equal(gResponseCounter, 4); + equal(g200Counter, 1, "check number of 200 responses"); + equal(g304Counter, 3, "check number of 304 responses"); + + // Sets a high delay for the cache fetch, and triggers the network activity. + channel = make_channel("http://localhost:" + PORT + "/rcwn"); + channel + .QueryInterface(Ci.nsIRaceCacheWithNetwork) + .test_delayCacheEntryOpeningBy(100000); + channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50); + channel.asyncOpen(new ChannelListener(checkContent, null)); + // Trigger network after 50 ms. + yield undefined; + equal(gResponseCounter, 5); + equal(g200Counter, 2, "check number of 200 responses"); + equal(g304Counter, 3, "check number of 304 responses"); + + // Sets a high delay for the cache fetch, and triggers the network activity. + // While the network response is produced, we trigger the cache fetch. + // Because the network response was the first, a non-conditional request is sent. + channel = make_channel("http://localhost:" + PORT + "/rcwn"); + channel + .QueryInterface(Ci.nsIRaceCacheWithNetwork) + .test_delayCacheEntryOpeningBy(100000); + channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50); + channel.asyncOpen(new ChannelListener(checkContent, null)); + yield undefined; + equal(gResponseCounter, 6); + equal(g200Counter, 3, "check number of 200 responses"); + equal(g304Counter, 3, "check number of 304 responses"); + + // Triggers cache open before triggering network. + channel = make_channel("http://localhost:" + PORT + "/rcwn"); + channel + .QueryInterface(Ci.nsIRaceCacheWithNetwork) + .test_delayCacheEntryOpeningBy(100000); + channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(5000); + channel.asyncOpen(new ChannelListener(checkContent, null)); + channel + .QueryInterface(Ci.nsIRaceCacheWithNetwork) + .test_triggerDelayedOpenCacheEntry(); + yield undefined; + equal(gResponseCounter, 7); + equal(g200Counter, 3, "check number of 200 responses"); + equal(g304Counter, 4, "check number of 304 responses"); + + // Load the cached handler so we don't need to revalidate + channel = make_channel("http://localhost:" + PORT + "/rcwn_cached"); + channel.asyncOpen(new ChannelListener(checkContent, null)); + yield undefined; + equal(gResponseCounter, 8); + equal(g200Counter, 4, "check number of 200 responses"); + equal(g304Counter, 4, "check number of 304 responses"); + + // Make sure response is loaded from the cache, not the network + channel = make_channel("http://localhost:" + PORT + "/rcwn_cached"); + channel.asyncOpen(new ChannelListener(checkContent, null)); + yield undefined; + equal(gResponseCounter, 9); + equal(g200Counter, 4, "check number of 200 responses"); + equal(g304Counter, 4, "check number of 304 responses"); + + // Cache times out, so we trigger the network + gIsFromCache = 0; + channel = make_channel("http://localhost:" + PORT + "/rcwn_cached"); + channel + .QueryInterface(Ci.nsIRaceCacheWithNetwork) + .test_delayCacheEntryOpeningBy(100000); + // trigger network after 50 ms + channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50); + channel.asyncOpen(new ChannelListener(checkContent, null)); + yield undefined; + equal(gResponseCounter, 10); + equal(gIsFromCache, 0, "should be from the network"); + equal(g200Counter, 5, "check number of 200 responses"); + equal(g304Counter, 4, "check number of 304 responses"); + + // Cache callback comes back right after network is triggered. + channel = make_channel("http://localhost:" + PORT + "/rcwn_cached"); + channel + .QueryInterface(Ci.nsIRaceCacheWithNetwork) + .test_delayCacheEntryOpeningBy(55); + channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50); + channel.asyncOpen(new ChannelListener(checkContent, null)); + yield undefined; + equal(gResponseCounter, 11); + info("IsFromCache: " + gIsFromCache + "\n"); + info("Number of 200 responses: " + g200Counter + "\n"); + equal(g304Counter, 4, "check number of 304 responses"); + + // Set an increasingly high timeout to trigger opening the cache entry + // This way we ensure that some of the entries we will get from the network, + // and some we will get from the cache. + gIsFromCache = 0; + for (var i = 0; i < 50; i++) { + channel = make_channel("http://localhost:" + PORT + "/rcwn_cached"); + channel + .QueryInterface(Ci.nsIRaceCacheWithNetwork) + .test_delayCacheEntryOpeningBy(i * 100); + channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(10); + channel.asyncOpen(new ChannelListener(checkContent, null)); + // This may be racy. The delay was chosen because the distribution of net-cache + // results was around 25-25 on my machine. + yield undefined; + } + + greater(gIsFromCache, 0, "Some of the responses should be from the cache"); + less(gIsFromCache, 50, "Some of the responses should be from the net"); + + httpserver.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_range_requests.js b/netwerk/test/unit/test_range_requests.js new file mode 100644 index 0000000000..1adc51f4d2 --- /dev/null +++ b/netwerk/test/unit/test_range_requests.js @@ -0,0 +1,441 @@ +// +// This test makes sure range-requests are sent and treated the way we want +// See bug #612135 for a thorough discussion on the subject +// +// Necko does a range-request for a partial cache-entry iff +// +// 1) size of the cached entry < value of the cached Content-Length header +// (not tested here - see bug #612135 comments 108-110) +// 2) the size of the cached entry is > 0 (see bug #628607) +// 3) the cached entry does not have a "no-store" Cache-Control header +// 4) the cached entry does not have a Content-Encoding (see bug #613159) +// 5) the request does not have a conditional-request header set by client +// 6) nsHttpResponseHead::IsResumable() is true for the cached entry +// 7) a basic positive test that makes sure byte ranges work +// 8) ensure NS_ERROR_CORRUPTED_CONTENT is thrown when total entity size +// of 206 does not match content-length of 200 +// +// The test has one handler for each case and run_tests() fires one request +// for each. None of the handlers should see a Range-header. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; + +const clearTextBody = "This is a slightly longer test\n"; +const encodedBody = [ + 0x1f, 0x8b, 0x08, 0x08, 0xef, 0x70, 0xe6, 0x4c, 0x00, 0x03, 0x74, 0x65, 0x78, + 0x74, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x74, 0x78, 0x74, 0x00, 0x0b, 0xc9, 0xc8, + 0x2c, 0x56, 0x00, 0xa2, 0x44, 0x85, 0xe2, 0x9c, 0xcc, 0xf4, 0x8c, 0x92, 0x9c, + 0x4a, 0x85, 0x9c, 0xfc, 0xbc, 0xf4, 0xd4, 0x22, 0x85, 0x92, 0xd4, 0xe2, 0x12, + 0x2e, 0x2e, 0x00, 0x00, 0xe5, 0xe6, 0xf0, 0x20, 0x00, 0x00, 0x00, +]; + +const partial_data_length = 4; +var port = null; // set in run_test + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +// StreamListener which cancels its request on first data available +function Canceler(continueFn) { + this.continueFn = continueFn; +} +Canceler.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + onStartRequest(request) {}, + + onDataAvailable(request, stream, offset, count) { + // Read stream so we don't assert for not reading from the stream + // if cancelling the channel is slow. + read_stream(stream, count); + + request.QueryInterface(Ci.nsIChannel).cancel(Cr.NS_BINDING_ABORTED); + }, + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_BINDING_ABORTED); + this.continueFn(request, null); + }, +}; +// Simple StreamListener which performs no validations +function MyListener(continueFn) { + this.continueFn = continueFn; + this._buffer = null; +} +MyListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + onStartRequest(request) { + this._buffer = ""; + }, + + onDataAvailable(request, stream, offset, count) { + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + onStopRequest(request, status) { + this.continueFn(request, this._buffer); + }, +}; + +var case_8_range_request = false; +function FailedChannelListener(continueFn) { + this.continueFn = continueFn; +} +FailedChannelListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + onStartRequest(request) {}, + + onDataAvailable(request, stream, offset, count) { + read_stream(stream, count); + }, + + onStopRequest(request, status) { + if (case_8_range_request) { + Assert.equal(status, Cr.NS_ERROR_CORRUPTED_CONTENT); + } + this.continueFn(request, null); + }, +}; + +function received_cleartext(request, data) { + Assert.equal(clearTextBody, data); + testFinished(); +} + +function setStdHeaders(response, length) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age: 360000"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "" + length); +} + +function handler_2(metadata, response) { + setStdHeaders(response, clearTextBody.length); + Assert.ok(!metadata.hasHeader("Range")); + response.bodyOutputStream.write(clearTextBody, clearTextBody.length); +} +function received_partial_2(request, data) { + Assert.equal(data, undefined); + var chan = make_channel("http://localhost:" + port + "/test_2"); + chan.asyncOpen(new ChannelListener(received_cleartext, null)); +} + +var case_3_request_no = 0; +function handler_3(metadata, response) { + var body = clearTextBody; + setStdHeaders(response, body.length); + response.setHeader("Cache-Control", "no-store", false); + switch (case_3_request_no) { + case 0: + Assert.ok(!metadata.hasHeader("Range")); + body = body.slice(0, partial_data_length); + response.processAsync(); + response.bodyOutputStream.write(body, body.length); + response.finish(); + break; + case 1: + Assert.ok(!metadata.hasHeader("Range")); + response.bodyOutputStream.write(body, body.length); + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_3_request_no++; +} +function received_partial_3(request, data) { + Assert.equal(partial_data_length, data.length); + var chan = make_channel("http://localhost:" + port + "/test_3"); + chan.asyncOpen(new ChannelListener(received_cleartext, null)); +} + +var case_4_request_no = 0; +function handler_4(metadata, response) { + switch (case_4_request_no) { + case 0: + Assert.ok(!metadata.hasHeader("Range")); + var body = encodedBody; + setStdHeaders(response, body.length); + response.setHeader("Content-Encoding", "gzip", false); + body = body.slice(0, partial_data_length); + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + bos.setOutputStream(response.bodyOutputStream); + response.processAsync(); + bos.writeByteArray(body); + response.finish(); + break; + case 1: + Assert.ok(!metadata.hasHeader("Range")); + setStdHeaders(response, clearTextBody.length); + response.bodyOutputStream.write(clearTextBody, clearTextBody.length); + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_4_request_no++; +} +function received_partial_4(request, data) { + // checking length does not work with encoded data + // do_check_eq(partial_data_length, data.length); + var chan = make_channel("http://localhost:" + port + "/test_4"); + chan.asyncOpen(new MyListener(received_cleartext)); +} + +var case_5_request_no = 0; +function handler_5(metadata, response) { + var body = clearTextBody; + setStdHeaders(response, body.length); + switch (case_5_request_no) { + case 0: + Assert.ok(!metadata.hasHeader("Range")); + body = body.slice(0, partial_data_length); + response.processAsync(); + response.bodyOutputStream.write(body, body.length); + response.finish(); + break; + case 1: + Assert.ok(!metadata.hasHeader("Range")); + response.bodyOutputStream.write(body, body.length); + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_5_request_no++; +} +function received_partial_5(request, data) { + Assert.equal(partial_data_length, data.length); + var chan = make_channel("http://localhost:" + port + "/test_5"); + chan.setRequestHeader("If-Match", "Some eTag", false); + chan.asyncOpen(new ChannelListener(received_cleartext, null)); +} + +var case_6_request_no = 0; +function handler_6(metadata, response) { + switch (case_6_request_no) { + case 0: + Assert.ok(!metadata.hasHeader("Range")); + var body = clearTextBody; + setStdHeaders(response, body.length); + response.setHeader("Accept-Ranges", "", false); + body = body.slice(0, partial_data_length); + response.processAsync(); + response.bodyOutputStream.write(body, body.length); + response.finish(); + break; + case 1: + Assert.ok(!metadata.hasHeader("Range")); + setStdHeaders(response, clearTextBody.length); + response.bodyOutputStream.write(clearTextBody, clearTextBody.length); + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_6_request_no++; +} +function received_partial_6(request, data) { + // would like to verify that the response does not have Accept-Ranges + Assert.equal(partial_data_length, data.length); + var chan = make_channel("http://localhost:" + port + "/test_6"); + chan.asyncOpen(new ChannelListener(received_cleartext, null)); +} + +const simpleBody = "0123456789"; + +function received_simple(request, data) { + Assert.equal(simpleBody, data); + testFinished(); +} + +var case_7_request_no = 0; +function handler_7(metadata, response) { + switch (case_7_request_no) { + case 0: + Assert.ok(!metadata.hasHeader("Range")); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "test7Etag"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Cache-Control", "max-age=360000"); + response.setHeader("Content-Length", "10"); + response.processAsync(); + response.bodyOutputStream.write(simpleBody.slice(0, 4), 4); + response.finish(); + break; + case 1: + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "test7Etag"); + if (metadata.hasHeader("Range")) { + Assert.ok(metadata.hasHeader("If-Range")); + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader("Content-Range", "4-9/10"); + response.setHeader("Content-Length", "6"); + response.bodyOutputStream.write(simpleBody.slice(4), 6); + } else { + response.setHeader("Content-Length", "10"); + response.bodyOutputStream.write(simpleBody, 10); + } + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_7_request_no++; +} +function received_partial_7(request, data) { + // make sure we get the first 4 bytes + Assert.equal(4, data.length); + // do it again to get the rest + var chan = make_channel("http://localhost:" + port + "/test_7"); + chan.asyncOpen(new ChannelListener(received_simple, null)); +} + +var case_8_request_no = 0; +function handler_8(metadata, response) { + switch (case_8_request_no) { + case 0: + Assert.ok(!metadata.hasHeader("Range")); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "test8Etag"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Cache-Control", "max-age=360000"); + response.setHeader("Content-Length", "10"); + response.processAsync(); + response.bodyOutputStream.write(simpleBody.slice(0, 4), 4); + response.finish(); + break; + case 1: + if (metadata.hasHeader("Range")) { + Assert.ok(metadata.hasHeader("If-Range")); + case_8_range_request = true; + } + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "test8Etag"); + response.setHeader("Content-Range", "4-8/9"); // intentionally broken + response.setHeader("Content-Length", "5"); + response.bodyOutputStream.write(simpleBody.slice(4), 5); + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_8_request_no++; +} +function received_partial_8(request, data) { + // make sure we get the first 4 bytes + Assert.equal(4, data.length); + // do it again to get the rest + var chan = make_channel("http://localhost:" + port + "/test_8"); + chan.asyncOpen( + new FailedChannelListener(testFinished, null, CL_EXPECT_LATE_FAILURE) + ); +} + +var case_9_request_no = 0; +function handler_9(metadata, response) { + switch (case_9_request_no) { + case 0: + Assert.ok(!metadata.hasHeader("Range")); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "W/test9WeakEtag"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Cache-Control", "max-age=360000"); + response.setHeader("Content-Length", "10"); + response.processAsync(); + response.bodyOutputStream.write(simpleBody.slice(0, 4), 4); + response.finish(); // truncated response + break; + case 1: + Assert.ok(!metadata.hasHeader("Range")); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "W/test9WeakEtag"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Cache-Control", "max-age=360000"); + response.setHeader("Content-Length", "10"); + response.processAsync(); + response.bodyOutputStream.write(simpleBody, 10); + response.finish(); // full response + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_9_request_no++; +} +function received_partial_9(request, data) { + Assert.equal(partial_data_length, data.length); + var chan = make_channel("http://localhost:" + port + "/test_9"); + chan.asyncOpen(new ChannelListener(received_simple, null)); +} + +// Simple mechanism to keep track of tests and stop the server +var numTestsFinished = 0; +function testFinished() { + if (++numTestsFinished == 7) { + httpserver.stop(do_test_finished); + } +} + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/test_2", handler_2); + httpserver.registerPathHandler("/test_3", handler_3); + httpserver.registerPathHandler("/test_4", handler_4); + httpserver.registerPathHandler("/test_5", handler_5); + httpserver.registerPathHandler("/test_6", handler_6); + httpserver.registerPathHandler("/test_7", handler_7); + httpserver.registerPathHandler("/test_8", handler_8); + httpserver.registerPathHandler("/test_9", handler_9); + httpserver.start(-1); + + port = httpserver.identity.primaryPort; + + // wipe out cached content + evict_cache_entries(); + + // Case 2: zero-length partial entry must not trigger range-request + let chan = make_channel("http://localhost:" + port + "/test_2"); + chan.asyncOpen(new Canceler(received_partial_2)); + + // Case 3: no-store response must not trigger range-request + chan = make_channel("http://localhost:" + port + "/test_3"); + chan.asyncOpen(new MyListener(received_partial_3)); + + // Case 4: response with content-encoding must not trigger range-request + chan = make_channel("http://localhost:" + port + "/test_4"); + chan.asyncOpen(new MyListener(received_partial_4)); + + // Case 5: conditional request-header set by client + chan = make_channel("http://localhost:" + port + "/test_5"); + chan.asyncOpen(new MyListener(received_partial_5)); + + // Case 6: response is not resumable (drop the Accept-Ranges header) + chan = make_channel("http://localhost:" + port + "/test_6"); + chan.asyncOpen(new MyListener(received_partial_6)); + + // Case 7: a basic positive test + chan = make_channel("http://localhost:" + port + "/test_7"); + chan.asyncOpen(new MyListener(received_partial_7)); + + // Case 8: check that mismatched 206 and 200 sizes throw error + chan = make_channel("http://localhost:" + port + "/test_8"); + chan.asyncOpen(new MyListener(received_partial_8)); + + // Case 9: check that weak etag is not used for a range request + chan = make_channel("http://localhost:" + port + "/test_9"); + chan.asyncOpen(new MyListener(received_partial_9)); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_rcwn_always_cache_new_content.js b/netwerk/test/unit/test_rcwn_always_cache_new_content.js new file mode 100644 index 0000000000..8244265088 --- /dev/null +++ b/netwerk/test/unit/test_rcwn_always_cache_new_content.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +httpserver.start(-1); +const PORT = httpserver.identity.primaryPort; + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +let gFirstResponseBody = "first version"; +let gSecondResponseBody = "second version"; +let gRequestCounter = 0; + +function test_handler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "max-age=3600"); + response.setHeader("ETag", "test-etag1"); + + switch (gRequestCounter) { + case 0: + response.bodyOutputStream.write( + gFirstResponseBody, + gFirstResponseBody.length + ); + break; + case 1: + response.bodyOutputStream.write( + gSecondResponseBody, + gSecondResponseBody.length + ); + break; + default: + do_throw("Unexpected request"); + } + response.setStatusLine(metadata.httpVersion, 200, "OK"); +} + +function checkContent(request, buffer, context, isFromCache) { + let isRacing = request.QueryInterface(Ci.nsICacheInfoChannel).isRacing(); + switch (gRequestCounter) { + case 0: + Assert.equal(buffer, gFirstResponseBody); + Assert.ok(!isFromCache); + Assert.ok(!isRacing); + break; + case 1: + Assert.equal(buffer, gSecondResponseBody); + Assert.ok(!isFromCache); + Assert.ok(isRacing); + break; + case 2: + Assert.equal(buffer, gSecondResponseBody); + Assert.ok(isFromCache); + Assert.ok(!isRacing); + break; + default: + do_throw("Unexpected request"); + } + + gRequestCounter++; + executeSoon(() => { + testGenerator.next(); + }); +} + +function run_test() { + do_get_profile(); + // In this test, we race the requests manually using |TriggerNetwork|, + // therefore we should disable racing in the HttpChannel first. + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + httpserver.registerPathHandler("/rcwn", test_handler); + testGenerator.next(); + do_test_pending(); +} + +let testGenerator = testSteps(); +function* testSteps() { + // Store first version of the content in the cache. + let channel = make_channel("http://localhost:" + PORT + "/rcwn"); + channel.asyncOpen(new ChannelListener(checkContent, null)); + yield undefined; + equal(gRequestCounter, 1); + + // Simulate the network victory by setting high delay for the cache fetch and + // triggering the network. + channel = make_channel("http://localhost:" + PORT + "/rcwn"); + channel + .QueryInterface(Ci.nsIRaceCacheWithNetwork) + .test_delayCacheEntryOpeningBy(100000); + // Trigger network after 50 ms. + channel.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50); + channel.asyncOpen(new ChannelListener(checkContent, null)); + yield undefined; + equal(gRequestCounter, 2); + + // Simulate navigation back by specifying VALIDATE_NEVER flag. + channel = make_channel("http://localhost:" + PORT + "/rcwn"); + channel.loadFlags = Ci.nsIRequest.VALIDATE_NEVER; + channel.asyncOpen(new ChannelListener(checkContent, null)); + yield undefined; + equal(gRequestCounter, 3); + + httpserver.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_rcwn_interrupted.js b/netwerk/test/unit/test_rcwn_interrupted.js new file mode 100644 index 0000000000..b761e26269 --- /dev/null +++ b/netwerk/test/unit/test_rcwn_interrupted.js @@ -0,0 +1,106 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits. +This is enhancement of 29a test, this test checks that cocurrency is resumed when the first channel is interrupted +in the middle of reading and the second channel already consumed some content from the cache entry. +This test is using a resumable response. +- with a profile, set max-entry-size to 1 (=1024 bytes) +- first channel makes a request for a resumable response +- second channel makes a request for the same resource, concurrent read happens +- first channel sets predicted data size on the entry with every chunk, it's doomed on 1024 +- second channel now must engage interrupted concurrent write algorithm and read the rest of the content from the network +- both channels must deliver full content w/o errors + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +// need something bigger than 1024 bytes +const responseBody = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +function contentHandler(metadata, response) { + response.processAsync(); + do_timeout(500, () => { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "" + responseBody.length); + if (metadata.hasHeader("If-Range")) { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + + let len = responseBody.length; + response.setHeader("Content-Range", "0-" + (len - 1) + "/" + len); + } + response.bodyOutputStream.write(responseBody, responseBody.length); + + response.finish(); + }); +} + +function run_test() { + // Static check + Assert.ok(responseBody.length > 1024); + + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function () { + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen( + new ChannelListener(firstTimeThrough, null, CL_IGNORE_DELAYS) + ); + var chan2 = make_channel(URL + "/content"); + chan2 + .QueryInterface(Ci.nsIRaceCacheWithNetwork) + .test_delayCacheEntryOpeningBy(200); + chan2.QueryInterface(Ci.nsIRaceCacheWithNetwork).test_triggerNetwork(50); + chan2.asyncOpen( + new ChannelListener(secondTimeThrough, null, CL_IGNORE_DELAYS) + ); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); +} + +function secondTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_readline.js b/netwerk/test/unit/test_readline.js new file mode 100644 index 0000000000..ac0c915406 --- /dev/null +++ b/netwerk/test/unit/test_readline.js @@ -0,0 +1,104 @@ +/* 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 PR_RDONLY = 0x1; + +function new_file_input_stream(file) { + var stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + stream.init(file, PR_RDONLY, 0, 0); + return stream; +} + +function new_line_input_stream(filename) { + return new_file_input_stream(do_get_file(filename)).QueryInterface( + Ci.nsILineInputStream + ); +} + +var test_array = [ + { file: "data/test_readline1.txt", lines: [] }, + { file: "data/test_readline2.txt", lines: [""] }, + { file: "data/test_readline3.txt", lines: ["", "", "", "", ""] }, + { file: "data/test_readline4.txt", lines: ["1", "23", "456", "", "78901"] }, + { + file: "data/test_readline5.txt", + lines: [ + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE", + ], + }, + { + file: "data/test_readline6.txt", + lines: [ + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE", + ], + }, + { + file: "data/test_readline7.txt", + lines: [ + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE", + "", + ], + }, + { + file: "data/test_readline8.txt", + lines: [ + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzSxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE", + ], + }, +]; + +function err(file, lineNo, msg) { + do_throw('"' + file + '" line ' + lineNo + ", " + msg); +} + +function run_test() { + for (var test of test_array) { + var lineStream = new_line_input_stream(test.file); + var lineNo = 0; + var more = false; + var line = {}; + more = lineStream.readLine(line); + for (var check of test.lines) { + ++lineNo; + if (lineNo == test.lines.length) { + if (more) { + err( + test.file, + lineNo, + "There should be no more data after the last line" + ); + } + } else if (!more) { + err(test.file, lineNo, "There should be more data after this line"); + } + if (line.value != check) { + err( + test.file, + lineNo, + "Wrong value, got '" + line.value + "' expected '" + check + "'" + ); + } + dump( + 'ok "' + + test.file + + '" line ' + + lineNo + + " (length " + + line.value.length + + "): '" + + line.value + + "'\n" + ); + more = lineStream.readLine(line); + } + if (more) { + err(test.file, lineNo, "'more' should be false after reading all lines"); + } + dump('ok "' + test.file + '" succeeded\n'); + lineStream.close(); + } +} diff --git a/netwerk/test/unit/test_redirect-caching_canceled.js b/netwerk/test/unit/test_redirect-caching_canceled.js new file mode 100644 index 0000000000..d1c3a77323 --- /dev/null +++ b/netwerk/test/unit/test_redirect-caching_canceled.js @@ -0,0 +1,62 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function () { + return URL + randomPath; +}); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", URL + "/content", false); + response.setHeader("Cache-control", "max-age=1000", false); +} + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + var chan = make_channel(randomURI); + chan.asyncOpen(new ChannelListener(secondTimeThrough, null)); +} + +function secondTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + var chan = make_channel(randomURI); + chan.loadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE; + chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT); + chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE)); +} + +function finish_test(request, buffer) { + Assert.equal(buffer, ""); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(randomPath, redirectHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan = make_channel(randomURI); + chan.asyncOpen(new ChannelListener(firstTimeThrough, null)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_redirect-caching_failure.js b/netwerk/test/unit/test_redirect-caching_failure.js new file mode 100644 index 0000000000..07d53cc585 --- /dev/null +++ b/netwerk/test/unit/test_redirect-caching_failure.js @@ -0,0 +1,79 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +/* + * The test is checking async redirect code path that is loading a cached + * redirect. But creation of the target channel fails before we even try + * to do async open on it. We force the creation error by forbidding + * the port number the URI contains. It must be done only after we have + * attempted to do the redirect (open the target URL) otherwise it's not + * cached. + */ + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function () { + return URL + randomPath; +}); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var serverRequestCount = 0; + +function redirectHandler(metadata, response) { + ++serverRequestCount; + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", "http://non-existent.tld:65400", false); + response.setHeader("Cache-control", "max-age=1000", false); +} + +function firstTimeThrough(request) { + Assert.equal(request.status, Cr.NS_ERROR_UNKNOWN_HOST); + Assert.equal(serverRequestCount, 1); + + const nextHop = () => { + var chan = make_channel(randomURI); + chan.loadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE; + chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE)); + }; + + if (inChildProcess()) { + do_send_remote_message("disable-ports"); + do_await_remote_message("disable-ports-done").then(nextHop); + } else { + Services.prefs.setCharPref("network.security.ports.banned", "65400"); + nextHop(); + } +} + +function finish_test(request, buffer) { + Assert.equal(request.status, Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED); + Assert.equal(serverRequestCount, 1); + Assert.equal(buffer, ""); + + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(randomPath, redirectHandler); + httpServer.start(-1); + + var chan = make_channel(randomURI); + chan.asyncOpen( + new ChannelListener(firstTimeThrough, null, CL_EXPECT_FAILURE) + ); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_redirect-caching_passing.js b/netwerk/test/unit/test_redirect-caching_passing.js new file mode 100644 index 0000000000..a19cf475e4 --- /dev/null +++ b/netwerk/test/unit/test_redirect-caching_passing.js @@ -0,0 +1,54 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function () { + return URL + randomPath; +}); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", URL + "/content", false); +} + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + var chan = make_channel(randomURI); + chan.loadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE; + chan.asyncOpen(new ChannelListener(finish_test, null)); +} + +function finish_test(request, buffer) { + Assert.equal(buffer, responseBody); + httpserver.stop(do_test_finished); +} + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler(randomPath, redirectHandler); + httpserver.registerPathHandler("/content", contentHandler); + httpserver.start(-1); + + var chan = make_channel(randomURI); + chan.asyncOpen(new ChannelListener(firstTimeThrough, null)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_redirect_baduri.js b/netwerk/test/unit/test_redirect_baduri.js new file mode 100644 index 0000000000..e3f4754344 --- /dev/null +++ b/netwerk/test/unit/test_redirect_baduri.js @@ -0,0 +1,42 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +/* + * Test whether we fail bad URIs in HTTP redirect as CORRUPTED_CONTENT. + */ + +var httpServer = null; + +var BadRedirectPath = "/BadRedirect"; +XPCOMUtils.defineLazyGetter(this, "BadRedirectURI", function () { + return ( + "http://localhost:" + httpServer.identity.primaryPort + BadRedirectPath + ); +}); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function BadRedirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + // '>' in URI will fail to parse: we should not render response + response.setHeader("Location", "http://localhost:4444>BadRedirect", false); +} + +function checkFailed(request, buffer) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(BadRedirectPath, BadRedirectHandler); + httpServer.start(-1); + + var chan = make_channel(BadRedirectURI); + chan.asyncOpen(new ChannelListener(checkFailed, null, CL_EXPECT_FAILURE)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_redirect_canceled.js b/netwerk/test/unit/test_redirect_canceled.js new file mode 100644 index 0000000000..db13950acd --- /dev/null +++ b/netwerk/test/unit/test_redirect_canceled.js @@ -0,0 +1,48 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function () { + return URL + randomPath; +}); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", URL + "/content", false); +} + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function finish_test(request, buffer) { + Assert.equal(buffer, ""); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(randomPath, redirectHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan = make_channel(randomURI); + chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT); + chan.asyncOpen(new ChannelListener(finish_test, null)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_redirect_different-protocol.js b/netwerk/test/unit/test_redirect_different-protocol.js new file mode 100644 index 0000000000..27dacb3b9e --- /dev/null +++ b/netwerk/test/unit/test_redirect_different-protocol.js @@ -0,0 +1,47 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function () { + return URL + randomPath; +}); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const redirectTargetBody = "response body"; +const response301Body = "redirect body"; + +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.bodyOutputStream.write(response301Body, response301Body.length); + response.setHeader( + "Location", + "data:text/plain," + redirectTargetBody, + false + ); +} + +function finish_test(request, buffer) { + Assert.equal(buffer, redirectTargetBody); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(randomPath, redirectHandler); + httpServer.start(-1); + + var chan = make_channel(randomURI); + chan.asyncOpen(new ChannelListener(finish_test, null, 0)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_redirect_failure.js b/netwerk/test/unit/test_redirect_failure.js new file mode 100644 index 0000000000..da9039278b --- /dev/null +++ b/netwerk/test/unit/test_redirect_failure.js @@ -0,0 +1,57 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +/* + * The test is checking async redirect code path that is loading a + * redirect. But creation of the target channel fails before we even try + * to do async open on it. We force the creation error by forbidding + * the port number the URI contains. + */ + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function () { + return URL + randomPath; +}); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", "http://non-existent.tld:65400", false); + response.setHeader("Cache-Control", "no-cache", false); +} + +function finish_test(request, buffer) { + Assert.equal(request.status, Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED); + + Assert.equal(buffer, ""); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(randomPath, redirectHandler); + httpServer.start(-1); + + if (!inChildProcess()) { + Services.prefs.setCharPref("network.security.ports.banned", "65400"); + } + + var chan = make_channel(randomURI); + chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_redirect_from_script.js b/netwerk/test/unit/test_redirect_from_script.js new file mode 100644 index 0000000000..d2ed886fd1 --- /dev/null +++ b/netwerk/test/unit/test_redirect_from_script.js @@ -0,0 +1,245 @@ +/* + * Test whether the rewrite-requests-from-script API implemented here: + * https://bugzilla.mozilla.org/show_bug.cgi?id=765934 is functioning + * correctly + * + * The test has the following components: + * + * testViaXHR() checks that internal redirects occur correctly for requests + * made with XMLHttpRequest objects. + * + * testViaAsyncOpen() checks that internal redirects occur correctly when made + * with nsIHTTPChannel.asyncOpen(). + * + * Both of the above functions tests four requests: + * + * Test 1: a simple case that redirects within a server; + * Test 2: a second that redirects to a second webserver; + * Test 3: internal script redirects in response to a server-side 302 redirect; + * Test 4: one internal script redirects in response to another's redirect. + * + * The successful redirects are confirmed by the presence of a custom response + * header. + * + */ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// the topic we observe to use the API. http-on-opening-request might also +// work for some purposes. +let redirectHook = "http-on-modify-request"; + +var httpServer = null, + httpServer2 = null; + +XPCOMUtils.defineLazyGetter(this, "port1", function () { + return httpServer.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "port2", function () { + return httpServer2.identity.primaryPort; +}); + +// Test Part 1: a cross-path redirect on a single HTTP server +// http://localhost:port1/bait -> http://localhost:port1/switch +var baitPath = "/bait"; +XPCOMUtils.defineLazyGetter(this, "baitURI", function () { + return "http://localhost:" + port1 + baitPath; +}); +var baitText = "you got the worm"; + +var redirectedPath = "/switch"; +XPCOMUtils.defineLazyGetter(this, "redirectedURI", function () { + return "http://localhost:" + port1 + redirectedPath; +}); +var redirectedText = "worms are not tasty"; + +// Test Part 2: Now, a redirect to a different server +// http://localhost:port1/bait2 -> http://localhost:port2/switch +var bait2Path = "/bait2"; +XPCOMUtils.defineLazyGetter(this, "bait2URI", function () { + return "http://localhost:" + port1 + bait2Path; +}); + +XPCOMUtils.defineLazyGetter(this, "redirected2URI", function () { + return "http://localhost:" + port2 + redirectedPath; +}); + +// Test Part 3, begin with a serverside redirect that itself turns into an instance +// of Test Part 1 +var bait3Path = "/bait3"; +XPCOMUtils.defineLazyGetter(this, "bait3URI", function () { + return "http://localhost:" + port1 + bait3Path; +}); + +// Test Part 4, begin with this client-side redirect and which then redirects +// to an instance of Test Part 1 +var bait4Path = "/bait4"; +XPCOMUtils.defineLazyGetter(this, "bait4URI", function () { + return "http://localhost:" + port1 + bait4Path; +}); + +var testHeaderName = "X-Redirected-By-Script"; +var testHeaderVal = "Success"; +var testHeaderVal2 = "Success on server 2"; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function baitHandler(metadata, response) { + // Content-Type required: https://bugzilla.mozilla.org/show_bug.cgi?id=748117 + response.setHeader("Content-Type", "text/html", false); + response.bodyOutputStream.write(baitText, baitText.length); +} + +function redirectedHandler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.bodyOutputStream.write(redirectedText, redirectedText.length); + response.setHeader(testHeaderName, testHeaderVal); +} + +function redirected2Handler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.bodyOutputStream.write(redirectedText, redirectedText.length); + response.setHeader(testHeaderName, testHeaderVal2); +} + +function bait3Handler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Location", baitURI); +} + +function Redirector() { + this.register(); +} + +Redirector.prototype = { + // This class observes an event and uses that to + // trigger a redirectTo(uri) redirect using the new API + register() { + Services.obs.addObserver(this, redirectHook, true); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + + observe(subject, topic, data) { + if (topic == redirectHook) { + if (!(subject instanceof Ci.nsIHttpChannel)) { + do_throw(redirectHook + " observed a non-HTTP channel"); + } + var channel = subject.QueryInterface(Ci.nsIHttpChannel); + var target = null; + if (channel.URI.spec == baitURI) { + target = redirectedURI; + } + if (channel.URI.spec == bait2URI) { + target = redirected2URI; + } + if (channel.URI.spec == bait4URI) { + target = baitURI; + } + // if we have a target, redirect there + if (target) { + var tURI = Services.io.newURI(target); + try { + channel.redirectTo(tURI); + } catch (e) { + do_throw("Exception in redirectTo " + e + "\n"); + } + } + } + }, +}; + +function makeAsyncTest(uri, headerValue, nextTask) { + // Make a test to check a redirect that is created with channel.asyncOpen() + + // Produce a callback function which checks for the presence of headerValue, + // and then continues to the next async test task + var verifier = function (req, buffer) { + if (!(req instanceof Ci.nsIHttpChannel)) { + do_throw(req + " is not an nsIHttpChannel, catastrophe imminent!"); + } + + var httpChannel = req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(httpChannel.getResponseHeader(testHeaderName), headerValue); + Assert.equal(buffer, redirectedText); + nextTask(); + }; + + // Produce a function to run an asyncOpen test using the above verifier + var test = function () { + var chan = make_channel(uri); + chan.asyncOpen(new ChannelListener(verifier)); + }; + return test; +} + +// will be defined in run_test because of the lazy getters, +// since the server's port is defined dynamically +var testViaAsyncOpen4 = null; +var testViaAsyncOpen3 = null; +var testViaAsyncOpen2 = null; +var testViaAsyncOpen = null; + +function testViaXHR() { + runXHRTest(baitURI, testHeaderVal); + runXHRTest(bait2URI, testHeaderVal2); + runXHRTest(bait3URI, testHeaderVal); + runXHRTest(bait4URI, testHeaderVal); +} + +function runXHRTest(uri, headerValue) { + // Check that making an XHR request for uri winds up redirecting to a result with the + // appropriate headerValue + var req = new XMLHttpRequest(); + req.open("GET", uri, false); + req.send(); + Assert.equal(req.getResponseHeader(testHeaderName), headerValue); + Assert.equal(req.response, redirectedText); +} + +function done() { + httpServer.stop(function () { + httpServer2.stop(do_test_finished); + }); +} + +// Needed for side-effects +new Redirector(); + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(baitPath, baitHandler); + httpServer.registerPathHandler(bait2Path, baitHandler); + httpServer.registerPathHandler(bait3Path, bait3Handler); + httpServer.registerPathHandler(bait4Path, baitHandler); + httpServer.registerPathHandler(redirectedPath, redirectedHandler); + httpServer.start(-1); + httpServer2 = new HttpServer(); + httpServer2.registerPathHandler(redirectedPath, redirected2Handler); + httpServer2.start(-1); + + // The tests depend on each other, and therefore need to be defined in the + // reverse of the order they are called in. It is therefore best to read this + // stanza backwards! + testViaAsyncOpen4 = makeAsyncTest(bait4URI, testHeaderVal, done); + testViaAsyncOpen3 = makeAsyncTest(bait3URI, testHeaderVal, testViaAsyncOpen4); + testViaAsyncOpen2 = makeAsyncTest( + bait2URI, + testHeaderVal2, + testViaAsyncOpen3 + ); + testViaAsyncOpen = makeAsyncTest(baitURI, testHeaderVal, testViaAsyncOpen2); + + testViaXHR(); + testViaAsyncOpen(); // will call done() asynchronously for cleanup + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_redirect_from_script_after-open_passing.js b/netwerk/test/unit/test_redirect_from_script_after-open_passing.js new file mode 100644 index 0000000000..9b15802787 --- /dev/null +++ b/netwerk/test/unit/test_redirect_from_script_after-open_passing.js @@ -0,0 +1,245 @@ +/* + * Test whether the rewrite-requests-from-script API implemented here: + * https://bugzilla.mozilla.org/show_bug.cgi?id=765934 is functioning + * correctly + * + * The test has the following components: + * + * testViaXHR() checks that internal redirects occur correctly for requests + * made with XMLHttpRequest objects. + * + * testViaAsyncOpen() checks that internal redirects occur correctly when made + * with nsIHTTPChannel.asyncOpen(). + * + * Both of the above functions tests four requests: + * + * Test 1: a simple case that redirects within a server; + * Test 2: a second that redirects to a second webserver; + * Test 3: internal script redirects in response to a server-side 302 redirect; + * Test 4: one internal script redirects in response to another's redirect. + * + * The successful redirects are confirmed by the presence of a custom response + * header. + * + */ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// the topic we observe to use the API. http-on-opening-request might also +// work for some purposes. +let redirectHook = "http-on-examine-response"; + +var httpServer = null, + httpServer2 = null; + +XPCOMUtils.defineLazyGetter(this, "port1", function () { + return httpServer.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "port2", function () { + return httpServer2.identity.primaryPort; +}); + +// Test Part 1: a cross-path redirect on a single HTTP server +// http://localhost:port1/bait -> http://localhost:port1/switch +var baitPath = "/bait"; +XPCOMUtils.defineLazyGetter(this, "baitURI", function () { + return "http://localhost:" + port1 + baitPath; +}); +var baitText = "you got the worm"; + +var redirectedPath = "/switch"; +XPCOMUtils.defineLazyGetter(this, "redirectedURI", function () { + return "http://localhost:" + port1 + redirectedPath; +}); +var redirectedText = "worms are not tasty"; + +// Test Part 2: Now, a redirect to a different server +// http://localhost:port1/bait2 -> http://localhost:port2/switch +var bait2Path = "/bait2"; +XPCOMUtils.defineLazyGetter(this, "bait2URI", function () { + return "http://localhost:" + port1 + bait2Path; +}); + +XPCOMUtils.defineLazyGetter(this, "redirected2URI", function () { + return "http://localhost:" + port2 + redirectedPath; +}); + +// Test Part 3, begin with a serverside redirect that itself turns into an instance +// of Test Part 1 +var bait3Path = "/bait3"; +XPCOMUtils.defineLazyGetter(this, "bait3URI", function () { + return "http://localhost:" + port1 + bait3Path; +}); + +// Test Part 4, begin with this client-side redirect and which then redirects +// to an instance of Test Part 1 +var bait4Path = "/bait4"; +XPCOMUtils.defineLazyGetter(this, "bait4URI", function () { + return "http://localhost:" + port1 + bait4Path; +}); + +var testHeaderName = "X-Redirected-By-Script"; +var testHeaderVal = "Success"; +var testHeaderVal2 = "Success on server 2"; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function baitHandler(metadata, response) { + // Content-Type required: https://bugzilla.mozilla.org/show_bug.cgi?id=748117 + response.setHeader("Content-Type", "text/html", false); + response.bodyOutputStream.write(baitText, baitText.length); +} + +function redirectedHandler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.bodyOutputStream.write(redirectedText, redirectedText.length); + response.setHeader(testHeaderName, testHeaderVal); +} + +function redirected2Handler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.bodyOutputStream.write(redirectedText, redirectedText.length); + response.setHeader(testHeaderName, testHeaderVal2); +} + +function bait3Handler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Location", baitURI); +} + +function Redirector() { + this.register(); +} + +Redirector.prototype = { + // This class observes an event and uses that to + // trigger a redirectTo(uri) redirect using the new API + register() { + Services.obs.addObserver(this, redirectHook, true); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + + observe(subject, topic, data) { + if (topic == redirectHook) { + if (!(subject instanceof Ci.nsIHttpChannel)) { + do_throw(redirectHook + " observed a non-HTTP channel"); + } + var channel = subject.QueryInterface(Ci.nsIHttpChannel); + var target = null; + if (channel.URI.spec == baitURI) { + target = redirectedURI; + } + if (channel.URI.spec == bait2URI) { + target = redirected2URI; + } + if (channel.URI.spec == bait4URI) { + target = baitURI; + } + // if we have a target, redirect there + if (target) { + var tURI = Services.io.newURI(target); + try { + channel.redirectTo(tURI); + } catch (e) { + do_throw("Exception in redirectTo " + e + "\n"); + } + } + } + }, +}; + +function makeAsyncTest(uri, headerValue, nextTask) { + // Make a test to check a redirect that is created with channel.asyncOpen() + + // Produce a callback function which checks for the presence of headerValue, + // and then continues to the next async test task + var verifier = function (req, buffer) { + if (!(req instanceof Ci.nsIHttpChannel)) { + do_throw(req + " is not an nsIHttpChannel, catastrophe imminent!"); + } + + var httpChannel = req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(httpChannel.getResponseHeader(testHeaderName), headerValue); + Assert.equal(buffer, redirectedText); + nextTask(); + }; + + // Produce a function to run an asyncOpen test using the above verifier + var test = function () { + var chan = make_channel(uri); + chan.asyncOpen(new ChannelListener(verifier)); + }; + return test; +} + +// will be defined in run_test because of the lazy getters, +// since the server's port is defined dynamically +var testViaAsyncOpen4 = null; +var testViaAsyncOpen3 = null; +var testViaAsyncOpen2 = null; +var testViaAsyncOpen = null; + +function testViaXHR() { + runXHRTest(baitURI, testHeaderVal); + runXHRTest(bait2URI, testHeaderVal2); + runXHRTest(bait3URI, testHeaderVal); + runXHRTest(bait4URI, testHeaderVal); +} + +function runXHRTest(uri, headerValue) { + // Check that making an XHR request for uri winds up redirecting to a result with the + // appropriate headerValue + var req = new XMLHttpRequest(); + req.open("GET", uri, false); + req.send(); + Assert.equal(req.getResponseHeader(testHeaderName), headerValue); + Assert.equal(req.response, redirectedText); +} + +function done() { + httpServer.stop(function () { + httpServer2.stop(do_test_finished); + }); +} + +// Needed for side-effects +new Redirector(); + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(baitPath, baitHandler); + httpServer.registerPathHandler(bait2Path, baitHandler); + httpServer.registerPathHandler(bait3Path, bait3Handler); + httpServer.registerPathHandler(bait4Path, baitHandler); + httpServer.registerPathHandler(redirectedPath, redirectedHandler); + httpServer.start(-1); + httpServer2 = new HttpServer(); + httpServer2.registerPathHandler(redirectedPath, redirected2Handler); + httpServer2.start(-1); + + // The tests depend on each other, and therefore need to be defined in the + // reverse of the order they are called in. It is therefore best to read this + // stanza backwards! + testViaAsyncOpen4 = makeAsyncTest(bait4URI, testHeaderVal, done); + testViaAsyncOpen3 = makeAsyncTest(bait3URI, testHeaderVal, testViaAsyncOpen4); + testViaAsyncOpen2 = makeAsyncTest( + bait2URI, + testHeaderVal2, + testViaAsyncOpen3 + ); + testViaAsyncOpen = makeAsyncTest(baitURI, testHeaderVal, testViaAsyncOpen2); + + testViaXHR(); + testViaAsyncOpen(); // will call done() asynchronously for cleanup + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_redirect_history.js b/netwerk/test/unit/test_redirect_history.js new file mode 100644 index 0000000000..986aff2675 --- /dev/null +++ b/netwerk/test/unit/test_redirect_history.js @@ -0,0 +1,73 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; +var redirects = []; +const numRedirects = 10; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function contentHandler(request, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function finish_test(request, buffer) { + Assert.equal(buffer, responseBody); + let chan = request.QueryInterface(Ci.nsIChannel); + let redirectChain = chan.loadInfo.redirectChain; + + Assert.equal(numRedirects - 1, redirectChain.length); + for (let i = 0; i < numRedirects - 1; ++i) { + let principal = redirectChain[i].principal; + Assert.equal(URL + redirects[i], principal.spec); + Assert.equal(redirectChain[i].referrerURI.spec, "http://test.com/"); + Assert.equal(redirectChain[i].remoteAddress, "127.0.0.1"); + } + httpServer.stop(do_test_finished); +} + +function redirectHandler(index, request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved"); + let path = redirects[index + 1]; + response.setHeader("Location", URL + path, false); +} + +function run_test() { + httpServer = new HttpServer(); + for (let i = 0; i < numRedirects; ++i) { + var randomPath = "/redirect/" + Math.random(); + redirects.push(randomPath); + if (i < numRedirects - 1) { + httpServer.registerPathHandler(randomPath, redirectHandler.bind(this, i)); + } else { + // The last one doesn't redirect + httpServer.registerPathHandler( + redirects[numRedirects - 1], + contentHandler + ); + } + } + httpServer.start(-1); + + var chan = make_channel(URL + redirects[0]); + var uri = NetUtil.newURI("http://test.com"); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri); + chan.asyncOpen(new ChannelListener(finish_test, null)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_redirect_loop.js b/netwerk/test/unit/test_redirect_loop.js new file mode 100644 index 0000000000..08ef96a2cb --- /dev/null +++ b/netwerk/test/unit/test_redirect_loop.js @@ -0,0 +1,86 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +/* + * This xpcshell test checks whether we detect infinite HTTP redirect loops. + * We check loops with "Location:" set to 1) full URI, 2) relative URI, and 3) + * empty Location header (which resolves to a relative link to the original + * URI when the original URI ends in a slash). + */ + +var httpServer = new HttpServer(); +httpServer.start(-1); +const PORT = httpServer.identity.primaryPort; + +var fullLoopPath = "/fullLoop"; +var fullLoopURI = "http://localhost:" + PORT + fullLoopPath; + +var relativeLoopPath = "/relativeLoop"; +var relativeLoopURI = "http://localhost:" + PORT + relativeLoopPath; + +// must use directory-style URI, so empty Location redirects back to self +var emptyLoopPath = "/empty/"; +var emptyLoopURI = "http://localhost:" + PORT + emptyLoopPath; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function fullLoopHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader( + "Location", + "http://localhost:" + PORT + "/fullLoop", + false + ); +} + +function relativeLoopHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", "relativeLoop", false); +} + +function emptyLoopHandler(metadata, response) { + // Comrades! We must seize power from the petty-bourgeois running dogs of + // httpd.js in order to reply with a blank Location header! + response.seizePower(); + response.write("HTTP/1.0 301 Moved\r\n"); + response.write("Location: \r\n"); + response.write("Content-Length: 4\r\n"); + response.write("\r\n"); + response.write("oops"); + response.finish(); +} + +function testFullLoop(request, buffer) { + Assert.equal(request.status, Cr.NS_ERROR_REDIRECT_LOOP); + + var chan = make_channel(relativeLoopURI); + chan.asyncOpen( + new ChannelListener(testRelativeLoop, null, CL_EXPECT_FAILURE) + ); +} + +function testRelativeLoop(request, buffer) { + Assert.equal(request.status, Cr.NS_ERROR_REDIRECT_LOOP); + + var chan = make_channel(emptyLoopURI); + chan.asyncOpen(new ChannelListener(testEmptyLoop, null, CL_EXPECT_FAILURE)); +} + +function testEmptyLoop(request, buffer) { + Assert.equal(request.status, Cr.NS_ERROR_REDIRECT_LOOP); + + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer.registerPathHandler(fullLoopPath, fullLoopHandler); + httpServer.registerPathHandler(relativeLoopPath, relativeLoopHandler); + httpServer.registerPathHandler(emptyLoopPath, emptyLoopHandler); + + var chan = make_channel(fullLoopURI); + chan.asyncOpen(new ChannelListener(testFullLoop, null, CL_EXPECT_FAILURE)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_redirect_passing.js b/netwerk/test/unit/test_redirect_passing.js new file mode 100644 index 0000000000..09c53117dd --- /dev/null +++ b/netwerk/test/unit/test_redirect_passing.js @@ -0,0 +1,53 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function () { + return URL + randomPath; +}); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", URL + "/content", false); +} + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + var chan = make_channel(randomURI); + chan.asyncOpen(new ChannelListener(finish_test, null)); +} + +function finish_test(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(randomPath, redirectHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan = make_channel(randomURI); + chan.asyncOpen(new ChannelListener(firstTimeThrough, null)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_redirect_protocol_telemetry.js b/netwerk/test/unit/test_redirect_protocol_telemetry.js new file mode 100644 index 0000000000..d7f9f93ca6 --- /dev/null +++ b/netwerk/test/unit/test_redirect_protocol_telemetry.js @@ -0,0 +1,63 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +add_task(async function check_protocols() { + // Enable the collection (during test) for all products so even products + // that don't collect the data will be able to run the test without failure. + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + let httpserv = new HttpServer(); + httpserv.registerPathHandler("/redirect", redirectHandler); + httpserv.registerPathHandler("/content", contentHandler); + httpserv.start(-1); + + var responseProtocol; + + function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + let location = + responseProtocol == "data" + ? "data:text/plain,test" + : `${responseProtocol}://localhost:${httpserv.identity.primaryPort}/content`; + response.setHeader("Location", location, false); + response.setHeader("Cache-Control", "no-cache", false); + } + + function contentHandler(metadata, response) { + let responseBody = "Content body"; + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); + } + + function make_test(protocol) { + do_get_profile(); + let redirect_hist = TelemetryTestUtils.getAndClearKeyedHistogram( + "NETWORK_HTTP_REDIRECT_TO_SCHEME" + ); + return new Promise(resolve => { + const URL = `http://localhost:${httpserv.identity.primaryPort}/redirect`; + responseProtocol = protocol; + let channel = make_channel(URL); + let p = new Promise(resolve1 => + channel.asyncOpen(new ChannelListener(resolve1)) + ); + p.then((request, buffer) => { + TelemetryTestUtils.assertKeyedHistogramSum(redirect_hist, protocol, 1); + resolve(); + }); + }); + } + + await make_test("http"); + await make_test("data"); + + await new Promise(resolve => httpserv.stop(resolve)); +}); diff --git a/netwerk/test/unit/test_redirect_veto.js b/netwerk/test/unit/test_redirect_veto.js new file mode 100644 index 0000000000..13841edfc7 --- /dev/null +++ b/netwerk/test/unit/test_redirect_veto.js @@ -0,0 +1,100 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function () { + return URL + randomPath; +}); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", URL + "/content", false); +} + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +let ChannelEventSink2 = { + _classDescription: "WebRequest channel event sink", + _classID: Components.ID("115062f8-92f1-11e5-8b7f-08001110f7ec"), + _contractID: "@mozilla.org/webrequest/channel-event-sink;1", + + QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink", "nsIFactory"]), + + init() { + Components.manager + .QueryInterface(Ci.nsIComponentRegistrar) + .registerFactory( + this._classID, + this._classDescription, + this._contractID, + this + ); + }, + + register() { + Services.catMan.addCategoryEntry( + "net-channel-event-sinks", + this._contractID, + this._contractID, + false, + true + ); + }, + + unregister() { + Services.catMan.deleteCategoryEntry( + "net-channel-event-sinks", + this._contractID, + false + ); + }, + + // nsIChannelEventSink implementation + asyncOnChannelRedirect(oldChannel, newChannel, flags, redirectCallback) { + // Abort the redirection + redirectCallback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE); + }, + + // nsIFactory implementation + createInstance(iid) { + return this.QueryInterface(iid); + }, +}; + +add_task(async function test_redirect_veto() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(randomPath, redirectHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + ChannelEventSink2.init(); + ChannelEventSink2.register(); + + let chan = make_channel(randomURI); + let [req, buff] = await new Promise(resolve => + chan.asyncOpen( + new ChannelListener((aReq, aBuff) => resolve([aReq, aBuff], null)) + ) + ); + Assert.equal(buff, ""); + Assert.equal(req.status, Cr.NS_OK); + await httpServer.stop(); + ChannelEventSink2.unregister(); +}); diff --git a/netwerk/test/unit/test_reentrancy.js b/netwerk/test/unit/test_reentrancy.js new file mode 100644 index 0000000000..3ac3570f8c --- /dev/null +++ b/netwerk/test/unit/test_reentrancy.js @@ -0,0 +1,107 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var testpath = "/simple"; +var httpbody = "<?xml version='1.0' ?><root>0123456789</root>"; + +function syncXHR() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", URL + testpath, false); + xhr.send(null); +} + +const MAX_TESTS = 2; + +var listener = { + _done_onStart: false, + _done_onData: false, + _test: 0, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + switch (this._test) { + case 0: + request.suspend(); + syncXHR(); + request.resume(); + break; + case 1: + request.suspend(); + syncXHR(); + executeSoon(function () { + request.resume(); + }); + break; + case 2: + executeSoon(function () { + request.suspend(); + }); + executeSoon(function () { + request.resume(); + }); + syncXHR(); + break; + } + + this._done_onStart = true; + }, + + onDataAvailable(request, stream, offset, count) { + Assert.ok(this._done_onStart); + read_stream(stream, count); + this._done_onData = true; + }, + + onStopRequest(request, status) { + Assert.ok(this._done_onData); + this._reset(); + if (this._test <= MAX_TESTS) { + next_test(); + } else { + httpserver.stop(do_test_finished); + } + }, + + _reset() { + this._done_onStart = false; + this._done_onData = false; + this._test++; + }, +}; + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function next_test() { + var chan = makeChan(URL + testpath); + chan.QueryInterface(Ci.nsIRequest); + chan.asyncOpen(listener); +} + +function run_test() { + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + + next_test(); + + do_test_pending(); +} + +function serverHandler(metadata, response) { + response.setHeader("Content-Type", "text/xml", false); + response.bodyOutputStream.write(httpbody, httpbody.length); +} diff --git a/netwerk/test/unit/test_referrer.js b/netwerk/test/unit/test_referrer.js new file mode 100644 index 0000000000..0c42849a43 --- /dev/null +++ b/netwerk/test/unit/test_referrer.js @@ -0,0 +1,248 @@ +"use strict"; + +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +function getTestReferrer(server_uri, referer_uri, isPrivate = false) { + var uri = NetUtil.newURI(server_uri); + let referrer = NetUtil.newURI(referer_uri); + let principal = Services.scriptSecurityManager.createContentPrincipal( + referrer, + { privateBrowsingId: isPrivate ? 1 : 0 } + ); + var chan = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + }); + + chan.QueryInterface(Ci.nsIHttpChannel); + chan.referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.EMPTY, + true, + referrer + ); + var header = null; + try { + header = chan.getRequestHeader("Referer"); + } catch (NS_ERROR_NOT_AVAILABLE) {} + return header; +} + +function run_test() { + var prefs = Services.prefs; + + var server_uri = "http://bar.examplesite.com/path2"; + var server_uri_2 = "http://bar.example.com/anotherpath"; + var referer_uri = "http://foo.example.com/path"; + var referer_uri_2 = "http://bar.examplesite.com/path3?q=blah"; + var referer_uri_2_anchor = "http://bar.examplesite.com/path3?q=blah#anchor"; + var referer_uri_idn = "http://sub1.\xe4lt.example/path"; + + // for https tests + var server_uri_https = "https://bar.example.com/anotherpath"; + var referer_uri_https = "https://bar.example.com/path3?q=blah"; + var referer_uri_2_https = "https://bar.examplesite.com/path3?q=blah"; + + // tests for sendRefererHeader + prefs.setIntPref("network.http.sendRefererHeader", 0); + Assert.equal(null, getTestReferrer(server_uri, referer_uri)); + prefs.setIntPref("network.http.sendRefererHeader", 2); + Assert.equal( + getTestReferrer(server_uri, referer_uri), + "http://foo.example.com/" + ); + + // test that https ref is not sent to http + Assert.equal(null, getTestReferrer(server_uri_2, referer_uri_https)); + + // tests for referer.defaultPolicy + prefs.setIntPref("network.http.referer.defaultPolicy", 0); + Assert.equal(null, getTestReferrer(server_uri, referer_uri)); + prefs.setIntPref("network.http.referer.defaultPolicy", 1); + Assert.equal(null, getTestReferrer(server_uri, referer_uri)); + Assert.equal(getTestReferrer(server_uri, referer_uri_2), referer_uri_2); + prefs.setIntPref("network.http.referer.defaultPolicy", 2); + Assert.equal(null, getTestReferrer(server_uri, referer_uri_https)); + Assert.equal( + getTestReferrer(server_uri_https, referer_uri_https), + referer_uri_https + ); + Assert.equal( + getTestReferrer(server_uri_https, referer_uri_2_https), + "https://bar.examplesite.com/" + ); + Assert.equal(getTestReferrer(server_uri, referer_uri_2), referer_uri_2); + Assert.equal( + getTestReferrer(server_uri, referer_uri), + "http://foo.example.com/" + ); + prefs.setIntPref("network.http.referer.defaultPolicy", 3); + Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri); + Assert.equal(null, getTestReferrer(server_uri_2, referer_uri_https)); + + // tests for referer.defaultPolicy.pbmode + prefs.setIntPref("network.http.referer.defaultPolicy.pbmode", 0); + Assert.equal(null, getTestReferrer(server_uri, referer_uri, true)); + prefs.setIntPref("network.http.referer.defaultPolicy.pbmode", 1); + Assert.equal(null, getTestReferrer(server_uri, referer_uri, true)); + Assert.equal(getTestReferrer(server_uri, referer_uri_2, true), referer_uri_2); + prefs.setIntPref("network.http.referer.defaultPolicy.pbmode", 2); + Assert.equal(null, getTestReferrer(server_uri, referer_uri_https, true)); + Assert.equal( + getTestReferrer(server_uri_https, referer_uri_https, true), + referer_uri_https + ); + Assert.equal( + getTestReferrer(server_uri_https, referer_uri_2_https, true), + "https://bar.examplesite.com/" + ); + Assert.equal(getTestReferrer(server_uri, referer_uri_2, true), referer_uri_2); + Assert.equal( + getTestReferrer(server_uri, referer_uri, true), + "http://foo.example.com/" + ); + prefs.setIntPref("network.http.referer.defaultPolicy.pbmode", 3); + Assert.equal(getTestReferrer(server_uri, referer_uri, true), referer_uri); + Assert.equal(null, getTestReferrer(server_uri_2, referer_uri_https, true)); + + // tests for referer.spoofSource + prefs.setBoolPref("network.http.referer.spoofSource", true); + Assert.equal(getTestReferrer(server_uri, referer_uri), server_uri); + prefs.setBoolPref("network.http.referer.spoofSource", false); + Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri); + + // tests for referer.XOriginPolicy + prefs.setIntPref("network.http.referer.XOriginPolicy", 2); + Assert.equal(null, getTestReferrer(server_uri_2, referer_uri)); + Assert.equal(getTestReferrer(server_uri, referer_uri_2), referer_uri_2); + prefs.setIntPref("network.http.referer.XOriginPolicy", 1); + Assert.equal(getTestReferrer(server_uri_2, referer_uri), referer_uri); + Assert.equal(null, getTestReferrer(server_uri, referer_uri)); + // https test + Assert.equal( + getTestReferrer(server_uri_https, referer_uri_https), + referer_uri_https + ); + prefs.setIntPref("network.http.referer.XOriginPolicy", 0); + Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri); + + // tests for referer.trimmingPolicy + prefs.setIntPref("network.http.referer.trimmingPolicy", 1); + Assert.equal( + getTestReferrer(server_uri, referer_uri_2), + "http://bar.examplesite.com/path3" + ); + Assert.equal( + getTestReferrer(server_uri, referer_uri_idn), + "http://sub1.xn--lt-uia.example/path" + ); + prefs.setIntPref("network.http.referer.trimmingPolicy", 2); + Assert.equal( + getTestReferrer(server_uri, referer_uri_2), + "http://bar.examplesite.com/" + ); + Assert.equal( + getTestReferrer(server_uri, referer_uri_idn), + "http://sub1.xn--lt-uia.example/" + ); + // https test + Assert.equal( + getTestReferrer(server_uri_https, referer_uri_https), + "https://bar.example.com/" + ); + prefs.setIntPref("network.http.referer.trimmingPolicy", 0); + // test that anchor is lopped off in ordinary case + Assert.equal( + getTestReferrer(server_uri, referer_uri_2_anchor), + referer_uri_2 + ); + + // tests for referer.XOriginTrimmingPolicy + prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 1); + Assert.equal( + getTestReferrer(server_uri, referer_uri), + "http://foo.example.com/path" + ); + Assert.equal( + getTestReferrer(server_uri, referer_uri_idn), + "http://sub1.xn--lt-uia.example/path" + ); + Assert.equal( + getTestReferrer(server_uri, referer_uri_2), + "http://bar.examplesite.com/path3?q=blah" + ); + prefs.setIntPref("network.http.referer.trimmingPolicy", 1); + Assert.equal( + getTestReferrer(server_uri, referer_uri_2), + "http://bar.examplesite.com/path3" + ); + prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 2); + Assert.equal( + getTestReferrer(server_uri, referer_uri), + "http://foo.example.com/" + ); + Assert.equal( + getTestReferrer(server_uri, referer_uri_idn), + "http://sub1.xn--lt-uia.example/" + ); + Assert.equal( + getTestReferrer(server_uri, referer_uri_2), + "http://bar.examplesite.com/path3" + ); + prefs.setIntPref("network.http.referer.trimmingPolicy", 0); + Assert.equal( + getTestReferrer(server_uri, referer_uri_2), + "http://bar.examplesite.com/path3?q=blah" + ); + // https tests + Assert.equal( + getTestReferrer(server_uri_https, referer_uri_https), + "https://bar.example.com/path3?q=blah" + ); + Assert.equal( + getTestReferrer(server_uri_https, referer_uri_2_https), + "https://bar.examplesite.com/" + ); + prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 0); + // test that anchor is lopped off in ordinary case + Assert.equal( + getTestReferrer(server_uri, referer_uri_2_anchor), + referer_uri_2 + ); + + // test referrer length limitation + // referer_uri's length is 27 and origin's length is 23 + prefs.setIntPref("network.http.referer.referrerLengthLimit", 27); + Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri); + prefs.setIntPref("network.http.referer.referrerLengthLimit", 26); + Assert.equal( + getTestReferrer(server_uri, referer_uri), + "http://foo.example.com/" + ); + prefs.setIntPref("network.http.referer.referrerLengthLimit", 22); + Assert.equal(getTestReferrer(server_uri, referer_uri), null); + prefs.setIntPref("network.http.referer.referrerLengthLimit", 0); + Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri); + prefs.setIntPref("network.http.referer.referrerLengthLimit", 4096); + Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri); + + // combination test: send spoofed path-only when hosts match + var combo_referer_uri = "http://blah.foo.com/path?q=hot"; + var dest_uri = "http://blah.foo.com:9999/spoofedpath?q=bad"; + prefs.setIntPref("network.http.referer.trimmingPolicy", 1); + prefs.setBoolPref("network.http.referer.spoofSource", true); + prefs.setIntPref("network.http.referer.XOriginPolicy", 2); + Assert.equal( + getTestReferrer(dest_uri, combo_referer_uri), + "http://blah.foo.com:9999/spoofedpath" + ); + Assert.equal( + null, + getTestReferrer(dest_uri, "http://gah.foo.com/anotherpath") + ); +} diff --git a/netwerk/test/unit/test_referrer_cross_origin.js b/netwerk/test/unit/test_referrer_cross_origin.js new file mode 100644 index 0000000000..ada64fcced --- /dev/null +++ b/netwerk/test/unit/test_referrer_cross_origin.js @@ -0,0 +1,332 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +function test_policy(test) { + info("Running test: " + test.toSource()); + + let prefs = Services.prefs; + + if (test.trimmingPolicy !== undefined) { + prefs.setIntPref( + "network.http.referer.trimmingPolicy", + test.trimmingPolicy + ); + } else { + prefs.setIntPref("network.http.referer.trimmingPolicy", 0); + } + + if (test.XOriginTrimmingPolicy !== undefined) { + prefs.setIntPref( + "network.http.referer.XOriginTrimmingPolicy", + test.XOriginTrimmingPolicy + ); + } else { + prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 0); + } + + if (test.disallowRelaxingDefault) { + prefs.setBoolPref( + "network.http.referer.disallowCrossSiteRelaxingDefault", + test.disallowRelaxingDefault + ); + } else { + prefs.setBoolPref( + "network.http.referer.disallowCrossSiteRelaxingDefault", + false + ); + } + + let referrer = NetUtil.newURI(test.referrer); + let triggeringPrincipal = + Services.scriptSecurityManager.createContentPrincipal(referrer, {}); + let chan = NetUtil.newChannel({ + uri: test.url, + loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + triggeringPrincipal, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + }); + + chan.QueryInterface(Ci.nsIHttpChannel); + chan.referrerInfo = new ReferrerInfo(test.policy, true, referrer); + + if (test.expectedReferrerSpec === undefined) { + try { + chan.getRequestHeader("Referer"); + do_throw("Should not find a Referer header!"); + } catch (e) {} + } else { + let header = chan.getRequestHeader("Referer"); + Assert.equal(header, test.expectedReferrerSpec); + } +} + +const nsIReferrerInfo = Ci.nsIReferrerInfo; +var gTests = [ + // Test same origin policy w/o cross origin + { + policy: nsIReferrerInfo.SAME_ORIGIN, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/foo?a", + }, + { + policy: nsIReferrerInfo.SAME_ORIGIN, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: undefined, + }, + { + policy: nsIReferrerInfo.SAME_ORIGIN, + trimmingPolicy: 1, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/foo", + }, + { + policy: nsIReferrerInfo.SAME_ORIGIN, + trimmingPolicy: 1, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: undefined, + }, + { + policy: nsIReferrerInfo.SAME_ORIGIN, + trimmingPolicy: 2, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/", + }, + { + policy: nsIReferrerInfo.SAME_ORIGIN, + trimmingPolicy: 2, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: undefined, + }, + + // Test origin when xorigin policy w/o cross origin + { + policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/foo?a", + }, + { + policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: "https://foo.example/", + }, + { + policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN, + trimmingPolicy: 1, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/foo", + }, + { + policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN, + trimmingPolicy: 1, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: "https://foo.example/", + }, + { + policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN, + trimmingPolicy: 2, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/", + }, + { + policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN, + trimmingPolicy: 2, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: "https://foo.example/", + }, + { + policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN, + XOriginTrimmingPolicy: 1, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/foo?a", + }, + { + policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN, + XOriginTrimmingPolicy: 1, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: "https://foo.example/", + }, + { + policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN, + XOriginTrimmingPolicy: 2, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/foo?a", + }, + { + policy: nsIReferrerInfo.ORIGIN_WHEN_CROSS_ORIGIN, + XOriginTrimmingPolicy: 2, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: "https://foo.example/", + }, + + // Test strict origin when xorigin policy w/o cross origin + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/foo?a", + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: "https://foo.example/", + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + url: "http://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: undefined, + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + trimmingPolicy: 1, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/foo", + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + trimmingPolicy: 1, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: "https://foo.example/", + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + trimmingPolicy: 1, + url: "http://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: undefined, + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + trimmingPolicy: 2, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/", + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + trimmingPolicy: 2, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: "https://foo.example/", + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + trimmingPolicy: 2, + url: "http://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: undefined, + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + XOriginTrimmingPolicy: 1, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/foo?a", + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + XOriginTrimmingPolicy: 1, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: "https://foo.example/", + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + XOriginTrimmingPolicy: 1, + url: "http://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: undefined, + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + XOriginTrimmingPolicy: 2, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/foo?a", + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + XOriginTrimmingPolicy: 2, + url: "https://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: "https://foo.example/", + }, + { + policy: nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + XOriginTrimmingPolicy: 2, + url: "http://test.example/foo?a", + referrer: "https://foo.example/foo?a", + expectedReferrerSpec: undefined, + }, + + // Test mix and choose max of XOriginTrimmingPolicy and trimmingPolicy + { + policy: nsIReferrerInfo.UNSAFE_URL, + XOriginTrimmingPolicy: 2, + trimmingPolicy: 1, + url: "https://test.example/foo?a", + referrer: "https://test1.example/foo?a", + expectedReferrerSpec: "https://test1.example/", + }, + { + policy: nsIReferrerInfo.UNSAFE_URL, + XOriginTrimmingPolicy: 2, + trimmingPolicy: 1, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/foo", + }, + { + policy: nsIReferrerInfo.UNSAFE_URL, + XOriginTrimmingPolicy: 1, + trimmingPolicy: 2, + url: "https://test.example/foo?a", + referrer: "https://test.example/foo?a", + expectedReferrerSpec: "https://test.example/", + }, + { + policy: nsIReferrerInfo.UNSAFE_URL, + XOriginTrimmingPolicy: 1, + trimmingPolicy: 0, + url: "https://test.example/foo?a", + referrer: "https://test1.example/foo?a", + expectedReferrerSpec: "https://test1.example/foo", + }, +]; + +function run_test() { + gTests.forEach(test => test_policy(test)); + Services.prefs.clearUserPref("network.http.referer.trimmingPolicy"); + Services.prefs.clearUserPref("network.http.referer.XOriginTrimmingPolicy"); + Services.prefs.clearUserPref( + "network.http.referer.disallowCrossSiteRelaxingDefault" + ); +} diff --git a/netwerk/test/unit/test_referrer_policy.js b/netwerk/test/unit/test_referrer_policy.js new file mode 100644 index 0000000000..18b1cb3a16 --- /dev/null +++ b/netwerk/test/unit/test_referrer_policy.js @@ -0,0 +1,154 @@ +"use strict"; + +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +function test_policy(test) { + info("Running test: " + test.toSource()); + + var prefs = Services.prefs; + if (test.defaultReferrerPolicyPref !== undefined) { + prefs.setIntPref( + "network.http.referer.defaultPolicy", + test.defaultReferrerPolicyPref + ); + } else { + prefs.setIntPref("network.http.referer.defaultPolicy", 3); + } + + if (test.disallowRelaxingDefault) { + prefs.setBoolPref( + "network.http.referer.disallowCrossSiteRelaxingDefault", + test.disallowRelaxingDefault + ); + } else { + prefs.setBoolPref( + "network.http.referer.disallowCrossSiteRelaxingDefault", + false + ); + } + + var uri = NetUtil.newURI(test.url); + var chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + + var referrer = NetUtil.newURI(test.referrer); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.referrerInfo = new ReferrerInfo(test.policy, true, referrer); + + if (test.expectedReferrerSpec === undefined) { + try { + chan.getRequestHeader("Referer"); + do_throw("Should not find a Referer header!"); + } catch (e) {} + } else { + var header = chan.getRequestHeader("Referer"); + Assert.equal(header, test.expectedReferrerSpec); + } +} + +const nsIReferrerInfo = Ci.nsIReferrerInfo; +// Assuming cross origin because we have no triggering principal available +var gTests = [ + { + policy: nsIReferrerInfo.EMPTY, + defaultReferrerPolicyPref: 0, + url: "https://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: undefined, + }, + { + policy: nsIReferrerInfo.EMPTY, + defaultReferrerPolicyPref: 1, + url: "http://test.example/foo", + referrer: "http://test1.example/referrer", + expectedReferrerSpec: undefined, + }, + { + policy: nsIReferrerInfo.EMPTY, + defaultReferrerPolicyPref: 2, + url: "https://sub1.\xe4lt.example/foo", + referrer: "https://sub1.\xe4lt.example/referrer", + expectedReferrerSpec: "https://sub1.xn--lt-uia.example/", + }, + { + policy: nsIReferrerInfo.EMPTY, + defaultReferrerPolicyPref: 2, + url: "https://test.example/foo", + referrer: "https://test1.example/referrer", + expectedReferrerSpec: "https://test1.example/", + }, + { + policy: nsIReferrerInfo.EMPTY, + defaultReferrerPolicyPref: 3, + url: "https://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: "https://test.example/referrer", + }, + { + policy: nsIReferrerInfo.EMPTY, + defaultReferrerPolicyPref: 3, + url: "https://sub1.\xe4lt.example/foo", + referrer: "https://sub1.\xe4lt.example/referrer", + expectedReferrerSpec: "https://sub1.xn--lt-uia.example/referrer", + }, + { + policy: nsIReferrerInfo.EMPTY, + defaultReferrerPolicyPref: 3, + url: "http://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: undefined, + }, + { + policy: nsIReferrerInfo.NO_REFERRER, + url: "https://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: undefined, + }, + { + policy: nsIReferrerInfo.ORIGIN, + url: "https://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: "https://test.example/", + }, + { + policy: nsIReferrerInfo.ORIGIN, + url: "https://sub1.\xe4lt.example/foo", + referrer: "https://sub1.\xe4lt.example/referrer", + expectedReferrerSpec: "https://sub1.xn--lt-uia.example/", + }, + { + policy: nsIReferrerInfo.UNSAFE_URL, + url: "https://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: "https://test.example/referrer", + }, + { + policy: nsIReferrerInfo.UNSAFE_URL, + url: "https://sub1.\xe4lt.example/foo", + referrer: "https://sub1.\xe4lt.example/referrer", + expectedReferrerSpec: "https://sub1.xn--lt-uia.example/referrer", + }, + { + policy: nsIReferrerInfo.UNSAFE_URL, + url: "http://test.example/foo", + referrer: "https://test.example/referrer", + expectedReferrerSpec: "https://test.example/referrer", + }, + { + policy: nsIReferrerInfo.UNSAFE_URL, + url: "http://sub1.\xe4lt.example/foo", + referrer: "https://sub1.\xe4lt.example/referrer", + expectedReferrerSpec: "https://sub1.xn--lt-uia.example/referrer", + }, +]; + +function run_test() { + gTests.forEach(test => test_policy(test)); + Services.prefs.clearUserPref("network.http.referer.disallowRelaxingDefault"); +} diff --git a/netwerk/test/unit/test_reopen.js b/netwerk/test/unit/test_reopen.js new file mode 100644 index 0000000000..eb43083ebb --- /dev/null +++ b/netwerk/test/unit/test_reopen.js @@ -0,0 +1,134 @@ +// This testcase verifies that channels can't be reopened +// See https://bugzilla.mozilla.org/show_bug.cgi?id=372486 + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +const NS_ERROR_IN_PROGRESS = 0x804b000f; +const NS_ERROR_ALREADY_OPENED = 0x804b0049; + +var chan = null; +var httpserv = null; + +[test_data_channel, test_http_channel, test_file_channel, end].forEach(f => + add_test(f) +); + +// Utility functions + +function makeChan(url) { + return (chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIChannel)); +} + +function new_file_channel(file) { + return NetUtil.newChannel({ + uri: Services.io.newFileURI(file), + loadUsingSystemPrincipal: true, + }); +} + +function check_throws(closure, error) { + var thrown = false; + try { + closure(); + } catch (e) { + if (error instanceof Array) { + Assert.notEqual(error.indexOf(e.result), -1); + } else { + Assert.equal(e.result, error); + } + thrown = true; + } + Assert.ok(thrown); +} + +function check_open_throws(error) { + check_throws(function () { + chan.open(listener); + }, error); +} + +function check_async_open_throws(error) { + check_throws(function () { + chan.asyncOpen(listener); + }, error); +} + +var listener = { + onStartRequest: function test_onStartR(request) { + check_async_open_throws(NS_ERROR_IN_PROGRESS); + }, + + onDataAvailable: function test_ODA(request, inputStream, offset, count) { + new BinaryInputStream(inputStream).readByteArray(count); // required by API + check_async_open_throws(NS_ERROR_IN_PROGRESS); + }, + + onStopRequest: function test_onStopR(request, status) { + // Once onStopRequest is reached, the channel is marked as having been + // opened + check_async_open_throws(NS_ERROR_ALREADY_OPENED); + do_timeout(0, after_channel_closed); + }, +}; + +function after_channel_closed() { + check_async_open_throws(NS_ERROR_ALREADY_OPENED); + + run_next_test(); +} + +function test_channel(createChanClosure) { + // First, synchronous reopening test + chan = createChanClosure(); + chan.open(); + check_open_throws(NS_ERROR_IN_PROGRESS); + check_async_open_throws([NS_ERROR_IN_PROGRESS, NS_ERROR_ALREADY_OPENED]); + + // Then, asynchronous one + chan = createChanClosure(); + chan.asyncOpen(listener); + check_open_throws(NS_ERROR_IN_PROGRESS); + check_async_open_throws(NS_ERROR_IN_PROGRESS); +} + +function test_data_channel() { + test_channel(function () { + return makeChan("data:text/plain,foo"); + }); +} + +function test_http_channel() { + test_channel(function () { + return makeChan("http://localhost:" + httpserv.identity.primaryPort + "/"); + }); +} + +function test_file_channel() { + var file = do_get_file("data/test_readline1.txt"); + test_channel(function () { + return new_file_channel(file); + }); +} + +function end() { + httpserv.stop(do_test_finished); +} + +function run_test() { + // start server + httpserv = new HttpServer(); + httpserv.start(-1); + + run_next_test(); +} diff --git a/netwerk/test/unit/test_reply_without_content_type.js b/netwerk/test/unit/test_reply_without_content_type.js new file mode 100644 index 0000000000..806e303dcd --- /dev/null +++ b/netwerk/test/unit/test_reply_without_content_type.js @@ -0,0 +1,149 @@ +// +// tests HTTP replies that lack content-type (where we try to sniff content-type). +// + +// Note: sets Cc and Ci variables +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var testpath = "/simple_plainText"; +var httpbody = "<html><body>omg hai</body></html>"; +var testpathGZip = "/simple_gzip"; +//this is compressed httpbody; +var httpbodyGZip = [ + "0x1f", + "0x8b", + "0x8", + "0x0", + "0x0", + "0x0", + "0x0", + "0x0", + "0x0", + "0x3", + "0xb3", + "0xc9", + "0x28", + "0xc9", + "0xcd", + "0xb1", + "0xb3", + "0x49", + "0xca", + "0x4f", + "0xa9", + "0xb4", + "0xcb", + "0xcf", + "0x4d", + "0x57", + "0xc8", + "0x48", + "0xcc", + "0xb4", + "0xd1", + "0x7", + "0xf3", + "0x6c", + "0xf4", + "0xc1", + "0x52", + "0x0", + "0x4", + "0x99", + "0x79", + "0x2b", + "0x21", + "0x0", + "0x0", + "0x0", +]; + +var dbg = 0; +if (dbg) { + print("============== START =========="); +} + +add_test(function test_plainText() { + if (dbg) { + print("============== test_plainText: in"); + } + httpserver.registerPathHandler(testpath, serverHandler_plainText); + httpserver.start(-1); + var channel = setupChannel(testpath); + // ChannelListener defined in head_channels.js + channel.asyncOpen(new ChannelListener(checkRequest, channel)); + do_test_pending(); + if (dbg) { + print("============== test_plainText: out"); + } +}); + +add_test(function test_GZip() { + if (dbg) { + print("============== test_GZip: in"); + } + httpserver.registerPathHandler(testpathGZip, serverHandler_GZip); + httpserver.start(-1); + var channel = setupChannel(testpathGZip); + // ChannelListener defined in head_channels.js + channel.asyncOpen(new ChannelListener(checkRequest, channel, CL_EXPECT_GZIP)); + do_test_pending(); + if (dbg) { + print("============== test_GZip: out"); + } +}); + +function setupChannel(path) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + path, + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + return chan; +} + +function serverHandler_plainText(metadata, response) { + if (dbg) { + print("============== serverHandler plainText: in"); + } + // no content type set + // response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(httpbody, httpbody.length); + if (dbg) { + print("============== serverHandler plainText: out"); + } +} + +function serverHandler_GZip(metadata, response) { + if (dbg) { + print("============== serverHandler GZip: in"); + } + // no content type set + // response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Encoding", "gzip", false); + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + bos.setOutputStream(response.bodyOutputStream); + bos.writeByteArray(httpbodyGZip); + if (dbg) { + print("============== serverHandler GZip: out"); + } +} + +function checkRequest(request, data, context) { + if (dbg) { + print("============== checkRequest: in"); + } + Assert.equal(data, httpbody); + Assert.equal(request.QueryInterface(Ci.nsIChannel).contentType, "text/html"); + httpserver.stop(do_test_finished); + run_next_test(); + if (dbg) { + print("============== checkRequest: out"); + } +} diff --git a/netwerk/test/unit/test_resumable_channel.js b/netwerk/test/unit/test_resumable_channel.js new file mode 100644 index 0000000000..bbbd62033b --- /dev/null +++ b/netwerk/test/unit/test_resumable_channel.js @@ -0,0 +1,424 @@ +/* Tests various aspects of nsIResumableChannel in combination with HTTP */ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = null; + +const NS_ERROR_ENTITY_CHANGED = 0x804b0020; +const NS_ERROR_NOT_RESUMABLE = 0x804b0019; + +const rangeBody = "Body of the range request handler.\r\n"; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function AuthPrompt2() {} + +AuthPrompt2.prototype = { + user: "guest", + pass: "guest", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + + promptAuth: function ap2_promptAuth(channel, level, authInfo) { + authInfo.username = this.user; + authInfo.password = this.pass; + return true; + }, + + asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +function Requestor() {} + +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface: function requestor_gi(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + // Allow the prompt to store state by caching it here + if (!this.prompt2) { + this.prompt2 = new AuthPrompt2(); + } + return this.prompt2; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompt2: null, +}; + +function run_test() { + dump("*** run_test\n"); + httpserver = new HttpServer(); + httpserver.registerPathHandler("/auth", authHandler); + httpserver.registerPathHandler("/range", rangeHandler); + httpserver.registerPathHandler("/acceptranges", acceptRangesHandler); + httpserver.registerPathHandler("/redir", redirHandler); + + var entityID; + + function get_entity_id(request, data, ctx) { + dump("*** get_entity_id()\n"); + Assert.ok( + request instanceof Ci.nsIResumableChannel, + "must be a resumable channel" + ); + entityID = request.entityID; + dump("*** entity id = " + entityID + "\n"); + + // Try a non-resumable URL (responds with 200) + var chan = make_channel(URL); + chan.nsIResumableChannel.resumeAt(1, entityID); + chan.asyncOpen(new ChannelListener(try_resume, null, CL_EXPECT_FAILURE)); + } + + function try_resume(request, data, ctx) { + dump("*** try_resume()\n"); + Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE); + + // Try a successful resume + var chan = make_channel(URL + "/range"); + chan.nsIResumableChannel.resumeAt(1, entityID); + chan.asyncOpen(new ChannelListener(try_resume_zero, null)); + } + + function try_resume_zero(request, data, ctx) { + dump("*** try_resume_zero()\n"); + Assert.ok(request.nsIHttpChannel.requestSucceeded); + Assert.equal(data, rangeBody.substring(1)); + + // Try a server which doesn't support range requests + var chan = make_channel(URL + "/acceptranges"); + chan.nsIResumableChannel.resumeAt(0, entityID); + chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "none", false); + chan.asyncOpen(new ChannelListener(try_no_range, null, CL_EXPECT_FAILURE)); + } + + function try_no_range(request, data, ctx) { + dump("*** try_no_range()\n"); + Assert.ok(request.nsIHttpChannel.requestSucceeded); + Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE); + + // Try a server which supports "bytes" range requests + var chan = make_channel(URL + "/acceptranges"); + chan.nsIResumableChannel.resumeAt(0, entityID); + chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "bytes", false); + chan.asyncOpen(new ChannelListener(try_bytes_range, null)); + } + + function try_bytes_range(request, data, ctx) { + dump("*** try_bytes_range()\n"); + Assert.ok(request.nsIHttpChannel.requestSucceeded); + Assert.equal(data, rangeBody); + + // Try a server which supports "foo" and "bar" range requests + var chan = make_channel(URL + "/acceptranges"); + chan.nsIResumableChannel.resumeAt(0, entityID); + chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foo, bar", false); + chan.asyncOpen( + new ChannelListener(try_foo_bar_range, null, CL_EXPECT_FAILURE) + ); + } + + function try_foo_bar_range(request, data, ctx) { + dump("*** try_foo_bar_range()\n"); + Assert.ok(request.nsIHttpChannel.requestSucceeded); + Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE); + + // Try a server which supports "foobar" range requests + var chan = make_channel(URL + "/acceptranges"); + chan.nsIResumableChannel.resumeAt(0, entityID); + chan.nsIHttpChannel.setRequestHeader("X-Range-Type", "foobar", false); + chan.asyncOpen( + new ChannelListener(try_foobar_range, null, CL_EXPECT_FAILURE) + ); + } + + function try_foobar_range(request, data, ctx) { + dump("*** try_foobar_range()\n"); + Assert.ok(request.nsIHttpChannel.requestSucceeded); + Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE); + + // Try a server which supports "bytes" and "foobar" range requests + var chan = make_channel(URL + "/acceptranges"); + chan.nsIResumableChannel.resumeAt(0, entityID); + chan.nsIHttpChannel.setRequestHeader( + "X-Range-Type", + "bytes, foobar", + false + ); + chan.asyncOpen(new ChannelListener(try_bytes_foobar_range, null)); + } + + function try_bytes_foobar_range(request, data, ctx) { + dump("*** try_bytes_foobar_range()\n"); + Assert.ok(request.nsIHttpChannel.requestSucceeded); + Assert.equal(data, rangeBody); + + // Try a server which supports "bytesfoo" and "bar" range requests + var chan = make_channel(URL + "/acceptranges"); + chan.nsIResumableChannel.resumeAt(0, entityID); + chan.nsIHttpChannel.setRequestHeader( + "X-Range-Type", + "bytesfoo, bar", + false + ); + chan.asyncOpen( + new ChannelListener(try_bytesfoo_bar_range, null, CL_EXPECT_FAILURE) + ); + } + + function try_bytesfoo_bar_range(request, data, ctx) { + dump("*** try_bytesfoo_bar_range()\n"); + Assert.ok(request.nsIHttpChannel.requestSucceeded); + Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE); + + // Try a server which doesn't send Accept-Ranges header at all + var chan = make_channel(URL + "/acceptranges"); + chan.nsIResumableChannel.resumeAt(0, entityID); + chan.asyncOpen(new ChannelListener(try_no_accept_ranges, null)); + } + + function try_no_accept_ranges(request, data, ctx) { + dump("*** try_no_accept_ranges()\n"); + Assert.ok(request.nsIHttpChannel.requestSucceeded); + Assert.equal(data, rangeBody); + + // Try a successful suspend/resume from 0 + var chan = make_channel(URL + "/range"); + chan.nsIResumableChannel.resumeAt(0, entityID); + chan.asyncOpen( + new ChannelListener( + try_suspend_resume, + null, + CL_SUSPEND | CL_EXPECT_3S_DELAY + ) + ); + } + + function try_suspend_resume(request, data, ctx) { + dump("*** try_suspend_resume()\n"); + Assert.ok(request.nsIHttpChannel.requestSucceeded); + Assert.equal(data, rangeBody); + + // Try a successful resume from 0 + var chan = make_channel(URL + "/range"); + chan.nsIResumableChannel.resumeAt(0, entityID); + chan.asyncOpen(new ChannelListener(success, null)); + } + + function success(request, data, ctx) { + dump("*** success()\n"); + Assert.ok(request.nsIHttpChannel.requestSucceeded); + Assert.equal(data, rangeBody); + + // Authentication (no password; working resume) + // (should not give us any data) + var chan = make_channel(URL + "/range"); + chan.nsIResumableChannel.resumeAt(1, entityID); + chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false); + chan.asyncOpen( + new ChannelListener(test_auth_nopw, null, CL_EXPECT_FAILURE) + ); + } + + function test_auth_nopw(request, data, ctx) { + dump("*** test_auth_nopw()\n"); + Assert.ok(!request.nsIHttpChannel.requestSucceeded); + Assert.equal(request.status, NS_ERROR_ENTITY_CHANGED); + + // Authentication + not working resume + var chan = make_channel( + "http://guest:guest@localhost:" + + httpserver.identity.primaryPort + + "/auth" + ); + chan.nsIResumableChannel.resumeAt(1, entityID); + chan.notificationCallbacks = new Requestor(); + chan.asyncOpen(new ChannelListener(test_auth, null, CL_EXPECT_FAILURE)); + } + function test_auth(request, data, ctx) { + dump("*** test_auth()\n"); + Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE); + Assert.ok(request.nsIHttpChannel.responseStatus < 300); + + // Authentication + working resume + var chan = make_channel( + "http://guest:guest@localhost:" + + httpserver.identity.primaryPort + + "/range" + ); + chan.nsIResumableChannel.resumeAt(1, entityID); + chan.notificationCallbacks = new Requestor(); + chan.nsIHttpChannel.setRequestHeader("X-Need-Auth", "true", false); + chan.asyncOpen(new ChannelListener(test_auth_resume, null)); + } + + function test_auth_resume(request, data, ctx) { + dump("*** test_auth_resume()\n"); + Assert.equal(data, rangeBody.substring(1)); + Assert.ok(request.nsIHttpChannel.requestSucceeded); + + // 404 page (same content length as real content) + var chan = make_channel(URL + "/range"); + chan.nsIResumableChannel.resumeAt(1, entityID); + chan.nsIHttpChannel.setRequestHeader("X-Want-404", "true", false); + chan.asyncOpen(new ChannelListener(test_404, null, CL_EXPECT_FAILURE)); + } + + function test_404(request, data, ctx) { + dump("*** test_404()\n"); + Assert.equal(request.status, NS_ERROR_ENTITY_CHANGED); + Assert.equal(request.nsIHttpChannel.responseStatus, 404); + + // 416 Requested Range Not Satisfiable + var chan = make_channel(URL + "/range"); + chan.nsIResumableChannel.resumeAt(1000, entityID); + chan.asyncOpen(new ChannelListener(test_416, null, CL_EXPECT_FAILURE)); + } + + function test_416(request, data, ctx) { + dump("*** test_416()\n"); + Assert.equal(request.status, NS_ERROR_ENTITY_CHANGED); + Assert.equal(request.nsIHttpChannel.responseStatus, 416); + + // Redirect + successful resume + var chan = make_channel(URL + "/redir"); + chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/range", false); + chan.nsIResumableChannel.resumeAt(1, entityID); + chan.asyncOpen(new ChannelListener(test_redir_resume, null)); + } + + function test_redir_resume(request, data, ctx) { + dump("*** test_redir_resume()\n"); + Assert.ok(request.nsIHttpChannel.requestSucceeded); + Assert.equal(data, rangeBody.substring(1)); + Assert.equal(request.nsIHttpChannel.responseStatus, 206); + + // Redirect + failed resume + var chan = make_channel(URL + "/redir"); + chan.nsIHttpChannel.setRequestHeader("X-Redir-To", URL + "/", false); + chan.nsIResumableChannel.resumeAt(1, entityID); + chan.asyncOpen( + new ChannelListener(test_redir_noresume, null, CL_EXPECT_FAILURE) + ); + } + + function test_redir_noresume(request, data, ctx) { + dump("*** test_redir_noresume()\n"); + Assert.equal(request.status, NS_ERROR_NOT_RESUMABLE); + + httpserver.stop(do_test_finished); + } + + httpserver.start(-1); + var chan = make_channel(URL + "/range"); + chan.asyncOpen(new ChannelListener(get_entity_id, null)); + do_test_pending(); +} + +// HANDLERS + +function handleAuth(metadata, response) { + // btoa("guest:guest"), but that function is not available here + var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; + + if ( + metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader + ) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + return true; + } + // didn't know guest:guest, failure + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + return false; +} + +// /auth +function authHandler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + var body = handleAuth(metadata, response) ? "success" : "failure"; + response.bodyOutputStream.write(body, body.length); +} + +// /range +function rangeHandler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + + if (metadata.hasHeader("X-Need-Auth")) { + if (!handleAuth(metadata, response)) { + body = "auth failed"; + response.bodyOutputStream.write(body, body.length); + return; + } + } + + if (metadata.hasHeader("X-Want-404")) { + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + body = rangeBody; + response.bodyOutputStream.write(body, body.length); + return; + } + + var body = rangeBody; + + if (metadata.hasHeader("Range")) { + // Syntax: bytes=[from]-[to] (we don't support multiple ranges) + var matches = metadata + .getHeader("Range") + .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/); + var from = matches[1] === undefined ? 0 : matches[1]; + var to = matches[2] === undefined ? rangeBody.length - 1 : matches[2]; + if (from >= rangeBody.length) { + response.setStatusLine(metadata.httpVersion, 416, "Start pos too high"); + response.setHeader("Content-Range", "*/" + rangeBody.length, false); + return; + } + body = body.substring(from, to + 1); + // always respond to successful range requests with 206 + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader( + "Content-Range", + from + "-" + to + "/" + rangeBody.length, + false + ); + } + + response.bodyOutputStream.write(body, body.length); +} + +// /acceptranges +function acceptRangesHandler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + if (metadata.hasHeader("X-Range-Type")) { + response.setHeader( + "Accept-Ranges", + metadata.getHeader("X-Range-Type"), + false + ); + } + response.bodyOutputStream.write(rangeBody, rangeBody.length); +} + +// /redir +function redirHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Location", metadata.getHeader("X-Redir-To"), false); + var body = "redirect\r\n"; + response.bodyOutputStream.write(body, body.length); +} diff --git a/netwerk/test/unit/test_resumable_truncate.js b/netwerk/test/unit/test_resumable_truncate.js new file mode 100644 index 0000000000..72785a5b4b --- /dev/null +++ b/netwerk/test/unit/test_resumable_truncate.js @@ -0,0 +1,93 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function cachedHandler(metadata, response) { + var body = responseBody; + if (metadata.hasHeader("Range")) { + var matches = metadata + .getHeader("Range") + .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/); + var from = matches[1] === undefined ? 0 : matches[1]; + var to = matches[2] === undefined ? responseBody.length - 1 : matches[2]; + if (from >= responseBody.length) { + response.setStatusLine(metadata.httpVersion, 416, "Start pos too high"); + response.setHeader("Content-Range", "*/" + responseBody.length, false); + return; + } + body = responseBody.slice(from, to + 1); + // always respond to successful range requests with 206 + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader( + "Content-Range", + from + "-" + to + "/" + responseBody.length, + false + ); + } + + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "Just testing"); + response.setHeader("Accept-Ranges", "bytes"); + + response.bodyOutputStream.write(body, body.length); +} + +function Canceler(continueFn) { + this.continueFn = continueFn; +} + +Canceler.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) {}, + + onDataAvailable(request, stream, offset, count) { + request.QueryInterface(Ci.nsIChannel).cancel(Cr.NS_BINDING_ABORTED); + }, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_BINDING_ABORTED); + this.continueFn(); + }, +}; + +function finish_test() { + httpserver.stop(do_test_finished); +} + +function start_cache_read() { + var chan = make_channel( + "http://localhost:" + httpserver.identity.primaryPort + "/cached/test.gz" + ); + chan.asyncOpen(new ChannelListener(finish_test, null)); +} + +function start_canceler() { + var chan = make_channel( + "http://localhost:" + httpserver.identity.primaryPort + "/cached/test.gz" + ); + chan.asyncOpen(new Canceler(start_cache_read)); +} + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/cached/test.gz", cachedHandler); + httpserver.start(-1); + + var chan = make_channel( + "http://localhost:" + httpserver.identity.primaryPort + "/cached/test.gz" + ); + chan.asyncOpen(new ChannelListener(start_canceler, null)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_retry_0rtt.js b/netwerk/test/unit/test_retry_0rtt.js new file mode 100644 index 0000000000..3ccb8b9c11 --- /dev/null +++ b/netwerk/test/unit/test_retry_0rtt.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"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +var httpServer = null; + +let handlerCallbacks = {}; + +function listenHandler(metadata, response) { + info(metadata.path); + handlerCallbacks[metadata.path] = (handlerCallbacks[metadata.path] || 0) + 1; +} + +function handlerCount(path) { + return handlerCallbacks[path] || 0; +} + +ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs"); + +// Bug 1805371: Tests that require FaultyServer can't currently be built +// with system NSS. +add_setup( + { + skip_if: () => AppConstants.MOZ_SYSTEM_NSS, + }, + async () => { + httpServer = new HttpServer(); + httpServer.registerPrefixHandler("/callback/", listenHandler); + httpServer.start(-1); + + registerCleanupFunction(async () => { + await httpServer.stop(); + }); + + Services.env.set( + "FAULTY_SERVER_CALLBACK_PORT", + httpServer.identity.primaryPort + ); + Services.env.set("MOZ_TLS_SERVER_0RTT", "1"); + await asyncStartTLSTestServer( + "FaultyServer", + "../../../security/manager/ssl/tests/unit/test_faulty_server" + ); + let nssComponent = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent); + await nssComponent.asyncClearSSLExternalAndInternalSessionCache(); + } +); + +async function sleep(time) { + return new Promise(resolve => { + do_timeout(time * 1000, resolve); + }); +} + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + chan.asyncOpen( + new ChannelListener((req, buffer) => resolve([req, buffer]), null, flags) + ); + }); +} + +add_task( + { + skip_if: () => AppConstants.MOZ_SYSTEM_NSS, + }, + async function testRetry0Rtt() { + var retryDomains = [ + "0rtt-alert-bad-mac.example.com", + "0rtt-alert-protocol-version.example.com", + //"0rtt-alert-unexpected.example.com", // TODO(bug 1753204): uncomment this + ]; + + Services.prefs.setCharPref("network.dns.localDomains", retryDomains); + + Services.prefs.setBoolPref("network.ssl_tokens_cache_enabled", true); + + for (var i = 0; i < retryDomains.length; i++) { + { + let countOfEarlyData = handlerCount("/callback/1"); + let chan = makeChan(`https://${retryDomains[i]}:8443`); + let [, buf] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + ok(buf); + equal( + handlerCount("/callback/1"), + countOfEarlyData, + "no early data sent" + ); + } + + // The server has an anti-replay mechanism that prohibits it from + // accepting 0-RTT connections immediately at startup. + await sleep(1); + + { + let countOfEarlyData = handlerCount("/callback/1"); + let chan = makeChan(`https://${retryDomains[i]}:8443`); + let [, buf] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + ok(buf); + equal( + handlerCount("/callback/1"), + countOfEarlyData + 1, + "got early data" + ); + } + } + } +); diff --git a/netwerk/test/unit/test_safeoutputstream.js b/netwerk/test/unit/test_safeoutputstream.js new file mode 100644 index 0000000000..4925394ce4 --- /dev/null +++ b/netwerk/test/unit/test_safeoutputstream.js @@ -0,0 +1,70 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +function write_atomic(file, str) { + var stream = Cc[ + "@mozilla.org/network/atomic-file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + stream.init(file, -1, -1, 0); + do { + var written = stream.write(str, str.length); + if (written == str.length) { + break; + } + str = str.substring(written); + } while (1); + stream.QueryInterface(Ci.nsISafeOutputStream).finish(); + stream.close(); +} + +function write(file, str) { + var stream = Cc[ + "@mozilla.org/network/safe-file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + stream.init(file, -1, -1, 0); + do { + var written = stream.write(str, str.length); + if (written == str.length) { + break; + } + str = str.substring(written); + } while (1); + stream.QueryInterface(Ci.nsISafeOutputStream).finish(); + stream.close(); +} + +function checkFile(file, str) { + var stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + stream.init(file, -1, -1, 0); + + var scriptStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + scriptStream.init(stream); + + Assert.equal(scriptStream.read(scriptStream.available()), str); + scriptStream.close(); +} + +function run_test() { + var filename = "\u0913"; + var file = Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append(filename); + + write(file, "First write"); + checkFile(file, "First write"); + + write(file, "Second write"); + checkFile(file, "Second write"); + + write_atomic(file, "First write: Atomic"); + checkFile(file, "First write: Atomic"); + + write_atomic(file, "Second write: Atomic"); + checkFile(file, "Second write: Atomic"); +} diff --git a/netwerk/test/unit/test_safeoutputstream_append.js b/netwerk/test/unit/test_safeoutputstream_append.js new file mode 100644 index 0000000000..9716001bd2 --- /dev/null +++ b/netwerk/test/unit/test_safeoutputstream_append.js @@ -0,0 +1,45 @@ +/* atomic-file-output-stream and safe-file-output-stream should throw and + * exception if PR_APPEND is explicity specified without PR_TRUNCATE. */ +"use strict"; + +const PR_WRONLY = 0x02; +const PR_CREATE_FILE = 0x08; +const PR_APPEND = 0x10; +const PR_TRUNCATE = 0x20; + +function check_flag(file, contractID, flags, throws) { + let stream = Cc[contractID].createInstance(Ci.nsIFileOutputStream); + + if (throws) { + /* NS_ERROR_INVALID_ARG is reported as NS_ERROR_ILLEGAL_VALUE, since they + * are same value. */ + Assert.throws( + () => stream.init(file, flags, 0o644, 0), + /NS_ERROR_ILLEGAL_VALUE/ + ); + } else { + stream.init(file, flags, 0o644, 0); + stream.close(); + } +} + +function run_test() { + let filename = "test.txt"; + let file = Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append(filename); + + let tests = [ + [PR_WRONLY | PR_CREATE_FILE | PR_APPEND | PR_TRUNCATE, false], + [PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, false], + [PR_WRONLY | PR_CREATE_FILE | PR_APPEND, true], + [-1, false], + ]; + for (let contractID of [ + "@mozilla.org/network/atomic-file-output-stream;1", + "@mozilla.org/network/safe-file-output-stream;1", + ]) { + for (let [flags, throws] of tests) { + check_flag(file, contractID, flags, throws); + } + } +} diff --git a/netwerk/test/unit/test_schema_10_migration.js b/netwerk/test/unit/test_schema_10_migration.js new file mode 100644 index 0000000000..af50c967fd --- /dev/null +++ b/netwerk/test/unit/test_schema_10_migration.js @@ -0,0 +1,181 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test cookie database migration from version 10 (prerelease Gecko 2.0) to the +// current version, presently 12. +"use strict"; + +var test_generator = do_run_test(); + +function run_test() { + do_test_pending(); + test_generator.next(); +} + +function finish_test() { + executeSoon(function () { + test_generator.return(); + do_test_finished(); + }); +} + +function* do_run_test() { + // Set up a profile. + let profile = do_get_profile(); + + // Start the cookieservice, to force creation of a database. + // Get the sessionCookies to join the initialization in cookie thread + Services.cookies.sessionCookies; + + // Close the profile. + do_close_profile(test_generator); + yield; + + // Remove the cookie file in order to create another database file. + do_get_cookie_file(profile).remove(false); + + // Create a schema 10 database. + let schema10db = new CookieDatabaseConnection( + do_get_cookie_file(profile), + 10 + ); + + let now = Date.now() * 1000; + let futureExpiry = Math.round(now / 1e6 + 1000); + let pastExpiry = Math.round(now / 1e6 - 1000); + + // Populate it, with: + // 1) Unexpired, unique cookies. + for (let i = 0; i < 20; ++i) { + let cookie = new Cookie( + "oh" + i, + "hai", + "foo.com", + "/", + futureExpiry, + now, + now + i, + false, + false, + false + ); + + schema10db.insertCookie(cookie); + } + + // 2) Expired, unique cookies. + for (let i = 20; i < 40; ++i) { + let cookie = new Cookie( + "oh" + i, + "hai", + "bar.com", + "/", + pastExpiry, + now, + now + i, + false, + false, + false + ); + + schema10db.insertCookie(cookie); + } + + // 3) Many copies of the same cookie, some of which have expired and + // some of which have not. + for (let i = 40; i < 45; ++i) { + let cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + futureExpiry + i, + now, + now + i, + false, + false, + false + ); + + try { + schema10db.insertCookie(cookie); + } catch (e) {} + } + for (let i = 45; i < 50; ++i) { + let cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + pastExpiry - i, + now, + now + i, + false, + false, + false + ); + + try { + schema10db.insertCookie(cookie); + } catch (e) {} + } + for (let i = 50; i < 55; ++i) { + let cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + futureExpiry - i, + now, + now + i, + false, + false, + false + ); + + try { + schema10db.insertCookie(cookie); + } catch (e) {} + } + for (let i = 55; i < 60; ++i) { + let cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + pastExpiry + i, + now, + now + i, + false, + false, + false + ); + + try { + schema10db.insertCookie(cookie); + } catch (e) {} + } + + // Close it. + schema10db.close(); + schema10db = null; + + // Load the database, forcing migration to the current schema version. Then + // test the expected set of cookies: + do_load_profile(); + + // 1) All unexpired, unique cookies exist. + Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 20); + + // 2) All expired, unique cookies exist. + Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20); + + // 3) Only one cookie remains, and it's the one with the highest expiration + // time. + Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1); + let cookies = Services.cookies.getCookiesFromHost("baz.com", {}); + let cookie = cookies[0]; + Assert.equal(cookie.expiry, futureExpiry + 40); + + finish_test(); +} diff --git a/netwerk/test/unit/test_schema_2_migration.js b/netwerk/test/unit/test_schema_2_migration.js new file mode 100644 index 0000000000..7565339904 --- /dev/null +++ b/netwerk/test/unit/test_schema_2_migration.js @@ -0,0 +1,303 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test cookie database migration from version 2 (Gecko 1.9.3) to the current +// version, presently 4 (Gecko 2.0). +"use strict"; + +var test_generator = do_run_test(); + +function run_test() { + do_test_pending(); + test_generator.next(); +} + +function finish_test() { + executeSoon(function () { + test_generator.return(); + do_test_finished(); + }); +} + +function* do_run_test() { + // Set up a profile. + let profile = do_get_profile(); + + // Start the cookieservice, to force creation of a database. + // Get the sessionCookies to join the initialization in cookie thread + Services.cookies.sessionCookies; + + // Close the profile. + do_close_profile(test_generator); + yield; + + // Remove the cookie file in order to create another database file. + do_get_cookie_file(profile).remove(false); + + // Create a schema 2 database. + let schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2); + + let now = Date.now() * 1000; + let futureExpiry = Math.round(now / 1e6 + 1000); + let pastExpiry = Math.round(now / 1e6 - 1000); + + // Populate it, with: + // 1) Unexpired, unique cookies. + for (let i = 0; i < 20; ++i) { + let cookie = new Cookie( + "oh" + i, + "hai", + "foo.com", + "/", + futureExpiry, + now, + now + i, + false, + false, + false + ); + + schema2db.insertCookie(cookie); + } + + // 2) Expired, unique cookies. + for (let i = 20; i < 40; ++i) { + let cookie = new Cookie( + "oh" + i, + "hai", + "bar.com", + "/", + pastExpiry, + now, + now + i, + false, + false, + false + ); + + schema2db.insertCookie(cookie); + } + + // 3) Many copies of the same cookie, some of which have expired and + // some of which have not. + for (let i = 40; i < 45; ++i) { + let cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + futureExpiry + i, + now, + now + i, + false, + false, + false + ); + + schema2db.insertCookie(cookie); + } + for (let i = 45; i < 50; ++i) { + let cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + pastExpiry - i, + now, + now + i, + false, + false, + false + ); + + schema2db.insertCookie(cookie); + } + for (let i = 50; i < 55; ++i) { + let cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + futureExpiry - i, + now, + now + i, + false, + false, + false + ); + + schema2db.insertCookie(cookie); + } + for (let i = 55; i < 60; ++i) { + let cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + pastExpiry + i, + now, + now + i, + false, + false, + false + ); + + schema2db.insertCookie(cookie); + } + + // Close it. + schema2db.close(); + schema2db = null; + + // Load the database, forcing migration to the current schema version. Then + // test the expected set of cookies: + do_load_profile(); + + // 1) All unexpired, unique cookies exist. + Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 20); + + // 2) All expired, unique cookies exist. + Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20); + + // 3) Only one cookie remains, and it's the one with the highest expiration + // time. + Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1); + let cookies = Services.cookies.getCookiesFromHost("baz.com", {}); + let cookie = cookies[0]; + Assert.equal(cookie.expiry, futureExpiry + 44); + + do_close_profile(test_generator); + yield; + + // Open the database so we can execute some more schema 2 statements on it. + schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2); + + // Populate it with more cookies. + for (let i = 60; i < 80; ++i) { + let cookie = new Cookie( + "oh" + i, + "hai", + "foo.com", + "/", + futureExpiry, + now, + now + i, + false, + false, + false + ); + + schema2db.insertCookie(cookie); + } + for (let i = 80; i < 100; ++i) { + let cookie = new Cookie( + "oh" + i, + "hai", + "cat.com", + "/", + futureExpiry, + now, + now + i, + false, + false, + false + ); + + schema2db.insertCookie(cookie); + } + + // Attempt to add a cookie with the same (name, host, path) values as another + // cookie. This should succeed since we have a REPLACE clause for conflict on + // the unique index. + cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + futureExpiry, + now, + now + 100, + false, + false, + false + ); + + schema2db.insertCookie(cookie); + + // Check that there is, indeed, a singular cookie for baz.com. + Assert.equal(do_count_cookies_in_db(schema2db.db, "baz.com"), 1); + + // Close it. + schema2db.close(); + schema2db = null; + + // Back up the database, so we can test both asynchronous and synchronous + // loading separately. + let file = do_get_cookie_file(profile); + let copy = profile.clone(); + copy.append("cookies.sqlite.copy"); + file.copyTo(null, copy.leafName); + + // Load the database asynchronously, forcing a purge of the newly-added + // cookies. (Their baseDomain column will be NULL.) + do_load_profile(test_generator); + yield; + + // Test the expected set of cookies. + Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 40); + Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20); + Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1); + Assert.equal(Services.cookies.countCookiesFromHost("cat.com"), 20); + + do_close_profile(test_generator); + yield; + + // Copy the database back. + file.remove(false); + copy.copyTo(null, file.leafName); + + // Load the database host-at-a-time. + do_load_profile(); + + // Test the expected set of cookies. + Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 40); + Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20); + Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1); + Assert.equal(Services.cookies.countCookiesFromHost("cat.com"), 20); + + do_close_profile(test_generator); + yield; + + // Open the database and prove that they were deleted. + schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2); + Assert.equal(do_count_cookies_in_db(schema2db.db), 81); + Assert.equal(do_count_cookies_in_db(schema2db.db, "foo.com"), 40); + Assert.equal(do_count_cookies_in_db(schema2db.db, "bar.com"), 20); + schema2db.close(); + + // Copy the database back. + file.remove(false); + copy.copyTo(null, file.leafName); + + // Load the database synchronously, in its entirety. + do_load_profile(); + Assert.equal(do_count_cookies(), 81); + + // Test the expected set of cookies. + Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 40); + Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20); + Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1); + Assert.equal(Services.cookies.countCookiesFromHost("cat.com"), 20); + + do_close_profile(test_generator); + yield; + + // Open the database and prove that they were deleted. + schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2); + Assert.equal(do_count_cookies_in_db(schema2db.db), 81); + Assert.equal(do_count_cookies_in_db(schema2db.db, "foo.com"), 40); + Assert.equal(do_count_cookies_in_db(schema2db.db, "bar.com"), 20); + schema2db.close(); + + finish_test(); +} diff --git a/netwerk/test/unit/test_schema_3_migration.js b/netwerk/test/unit/test_schema_3_migration.js new file mode 100644 index 0000000000..7b5c639950 --- /dev/null +++ b/netwerk/test/unit/test_schema_3_migration.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test cookie database migration from version 3 (prerelease Gecko 2.0) to the +// current version, presently 4 (Gecko 2.0). +"use strict"; + +var test_generator = do_run_test(); + +function run_test() { + do_test_pending(); + test_generator.next(); +} + +function finish_test() { + executeSoon(function () { + test_generator.return(); + do_test_finished(); + }); +} + +function* do_run_test() { + // Set up a profile. + let profile = do_get_profile(); + + // Start the cookieservice, to force creation of a database. + // Get the sessionCookies to join the initialization in cookie thread + Services.cookies.sessionCookies; + + // Close the profile. + do_close_profile(test_generator); + yield; + + // Remove the cookie file in order to create another database file. + do_get_cookie_file(profile).remove(false); + + // Create a schema 3 database. + let schema3db = new CookieDatabaseConnection(do_get_cookie_file(profile), 3); + + let now = Date.now() * 1000; + let futureExpiry = Math.round(now / 1e6 + 1000); + let pastExpiry = Math.round(now / 1e6 - 1000); + + // Populate it, with: + // 1) Unexpired, unique cookies. + for (let i = 0; i < 20; ++i) { + let cookie = new Cookie( + "oh" + i, + "hai", + "foo.com", + "/", + futureExpiry, + now, + now + i, + false, + false, + false + ); + + schema3db.insertCookie(cookie); + } + + // 2) Expired, unique cookies. + for (let i = 20; i < 40; ++i) { + let cookie = new Cookie( + "oh" + i, + "hai", + "bar.com", + "/", + pastExpiry, + now, + now + i, + false, + false, + false + ); + + schema3db.insertCookie(cookie); + } + + // 3) Many copies of the same cookie, some of which have expired and + // some of which have not. + for (let i = 40; i < 45; ++i) { + let cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + futureExpiry + i, + now, + now + i, + false, + false, + false + ); + + schema3db.insertCookie(cookie); + } + for (let i = 45; i < 50; ++i) { + let cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + pastExpiry - i, + now, + now + i, + false, + false, + false + ); + + schema3db.insertCookie(cookie); + } + for (let i = 50; i < 55; ++i) { + let cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + futureExpiry - i, + now, + now + i, + false, + false, + false + ); + + schema3db.insertCookie(cookie); + } + for (let i = 55; i < 60; ++i) { + let cookie = new Cookie( + "oh", + "hai", + "baz.com", + "/", + pastExpiry + i, + now, + now + i, + false, + false, + false + ); + + schema3db.insertCookie(cookie); + } + + // Close it. + schema3db.close(); + schema3db = null; + + // Load the database, forcing migration to the current schema version. Then + // test the expected set of cookies: + do_load_profile(); + + // 1) All unexpired, unique cookies exist. + Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 20); + + // 2) All expired, unique cookies exist. + Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20); + + // 3) Only one cookie remains, and it's the one with the highest expiration + // time. + Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1); + let cookies = Services.cookies.getCookiesFromHost("baz.com", {}); + let cookie = cookies[0]; + Assert.equal(cookie.expiry, futureExpiry + 44); + + finish_test(); +} diff --git a/netwerk/test/unit/test_separate_connections.js b/netwerk/test/unit/test_separate_connections.js new file mode 100644 index 0000000000..2bf4358c2c --- /dev/null +++ b/netwerk/test/unit/test_separate_connections.js @@ -0,0 +1,102 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +// This unit test ensures each container has its own connection pool. +// We verify this behavior by opening channels with different userContextId, +// and their connection info's hash keys should be different. + +// In the first round of this test, we record the hash key in each container. +// In the second round, we check if each container's hash key is consistent +// and different from other container's hash key. + +let httpserv = null; +let gSecondRoundStarted = false; + +function handler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + let body = "0123456789"; + response.bodyOutputStream.write(body, body.length); +} + +function makeChan(url, userContextId) { + let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); + chan.loadInfo.originAttributes = { userContextId }; + return chan; +} + +let previousHashKeys = []; + +function Listener(userContextId) { + this.userContextId = userContextId; +} + +let gTestsRun = 0; +Listener.prototype = { + onStartRequest(request) { + request + .QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + Assert.equal( + request.loadInfo.originAttributes.userContextId, + this.userContextId + ); + + let hashKey = request.connectionInfoHashKey; + if (gSecondRoundStarted) { + // Compare the hash keys with the previous set ones. + // Hash keys should match if and only if their userContextId are the same. + for (let userContextId = 0; userContextId < 3; userContextId++) { + if (userContextId == this.userContextId) { + Assert.equal(hashKey, previousHashKeys[userContextId]); + } else { + Assert.notEqual(hashKey, previousHashKeys[userContextId]); + } + } + } else { + // Set the hash keys in the first round. + previousHashKeys[this.userContextId] = hashKey; + } + }, + onDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + onStopRequest() { + gTestsRun++; + if (gTestsRun == 3) { + gTestsRun = 0; + if (gSecondRoundStarted) { + // The second round finishes. + httpserv.stop(do_test_finished); + } else { + // The first round finishes. Do the second round. + gSecondRoundStarted = true; + doTest(); + } + } + }, +}; + +function doTest() { + for (let userContextId = 0; userContextId < 3; userContextId++) { + let chan = makeChan(URL, userContextId); + let listener = new Listener(userContextId); + chan.asyncOpen(listener); + } +} + +function run_test() { + do_test_pending(); + httpserv = new HttpServer(); + httpserv.registerPathHandler("/", handler); + httpserv.start(-1); + + doTest(); +} diff --git a/netwerk/test/unit/test_servers.js b/netwerk/test/unit/test_servers.js new file mode 100644 index 0000000000..aa7afd6c8e --- /dev/null +++ b/netwerk/test/unit/test_servers.js @@ -0,0 +1,351 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function channelOpenPromise(chan, flags, observer) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +add_task(async function test_dual_stack() { + let httpserv = new HttpServer(); + let content = "ok"; + httpserv.registerPathHandler("/", function handler(metadata, response) { + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + }); + httpserv.start_dualStack(-1); + + let chan = makeChan(`http://127.0.0.1:${httpserv.identity.primaryPort}/`); + let [, response] = await channelOpenPromise(chan); + Assert.equal(response, content); + + chan = makeChan(`http://[::1]:${httpserv.identity.primaryPort}/`); + [, response] = await channelOpenPromise(chan); + Assert.equal(response, content); + await new Promise(resolve => httpserv.stop(resolve)); +}); + +add_task(async function test_http() { + let server = new NodeHTTPServer(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + let chan = makeChan(`http://localhost:${server.port()}/test`); + let req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 404); + await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200); + resp.end("done"); + }); + chan = makeChan(`http://localhost:${server.port()}/test`); + req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + equal(req.QueryInterface(Ci.nsIHttpChannel).protocolVersion, "http/1.1"); + equal(req.QueryInterface(Ci.nsIHttpChannelInternal).isProxyUsed, false); + + await server.stop(); +}); + +add_task(async function test_https() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + let server = new NodeHTTPSServer(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + let chan = makeChan(`https://localhost:${server.port()}/test`); + let req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 404); + await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200); + resp.end("done"); + }); + chan = makeChan(`https://localhost:${server.port()}/test`); + req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + equal(req.QueryInterface(Ci.nsIHttpChannel).protocolVersion, "http/1.1"); + + await server.stop(); +}); + +add_task(async function test_http2() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + let server = new NodeHTTP2Server(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + let chan = makeChan(`https://localhost:${server.port()}/test`); + let req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 404); + await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200); + resp.end("done"); + }); + chan = makeChan(`https://localhost:${server.port()}/test`); + req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + equal(req.QueryInterface(Ci.nsIHttpChannel).protocolVersion, "h2"); + + await server.stop(); +}); + +add_task(async function test_http1_proxy() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + let proxy = new NodeHTTPProxyServer(); + await proxy.start(); + registerCleanupFunction(async () => { + await proxy.stop(); + }); + + let chan = makeChan(`http://localhost:${proxy.port()}/test`); + let req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 405); + + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + await server.execute( + `global.server_name = "${server.constructor.name}";` + ); + await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200); + resp.end(global.server_name); + }); + let chan = makeChan(`${server.origin()}/test`); + let { req, buff } = await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + (req, buff) => resolve({ req, buff }), + null, + CL_ALLOW_UNKNOWN_CL + ) + ); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + equal(buff, server.constructor.name); + //Bug 1792187: Check if proxy is set to true when a proxy is used. + equal(req.QueryInterface(Ci.nsIHttpChannelInternal).isProxyUsed, true); + equal( + req.QueryInterface(Ci.nsIHttpChannel).protocolVersion, + server.constructor.name == "NodeHTTP2Server" ? "h2" : "http/1.1" + ); + } + ); + + await proxy.stop(); +}); + +add_task(async function test_https_proxy() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + registerCleanupFunction(async () => { + await proxy.stop(); + }); + + let chan = makeChan(`https://localhost:${proxy.port()}/test`); + let req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 405); + + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + await server.execute( + `global.server_name = "${server.constructor.name}";` + ); + await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200); + resp.end(global.server_name); + }); + let chan = makeChan(`${server.origin()}/test`); + let { req, buff } = await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + (req, buff) => resolve({ req, buff }), + null, + CL_ALLOW_UNKNOWN_CL + ) + ); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + equal(buff, server.constructor.name); + } + ); + + await proxy.stop(); +}); + +add_task(async function test_http2_proxy() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); + registerCleanupFunction(async () => { + await proxy.stop(); + }); + + let chan = makeChan(`https://localhost:${proxy.port()}/test`); + let req = await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 405); + + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + await server.execute( + `global.server_name = "${server.constructor.name}";` + ); + await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200); + resp.end(global.server_name); + }); + let chan = makeChan(`${server.origin()}/test`); + let { req, buff } = await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + (req, buff) => resolve({ req, buff }), + null, + CL_ALLOW_UNKNOWN_CL + ) + ); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + equal(buff, server.constructor.name); + } + ); + + await proxy.stop(); +}); + +add_task(async function test_proxy_with_redirects() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + let proxies = [ + NodeHTTPProxyServer, + NodeHTTPSProxyServer, + NodeHTTP2ProxyServer, + ]; + for (let p of proxies) { + let proxy = new p(); + await proxy.start(); + registerCleanupFunction(async () => { + await proxy.stop(); + }); + + await with_node_servers( + [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], + async server => { + info(`Testing ${p.name} with ${server.constructor.name}`); + await server.execute( + `global.server_name = "${server.constructor.name}";` + ); + await server.registerPathHandler("/redirect", (req, resp) => { + resp.writeHead(302, { + Location: "/test", + }); + resp.end(global.server_name); + }); + await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200); + resp.end(global.server_name); + }); + + let chan = makeChan(`${server.origin()}/redirect`); + let { req, buff } = await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + (req, buff) => resolve({ req, buff }), + null, + CL_ALLOW_UNKNOWN_CL + ) + ); + }); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + equal(buff, server.constructor.name); + req.QueryInterface(Ci.nsIProxiedChannel); + ok(!!req.proxyInfo); + notEqual(req.proxyInfo.type, "direct"); + } + ); + await proxy.stop(); + } +}); diff --git a/netwerk/test/unit/test_signature_extraction.js b/netwerk/test/unit/test_signature_extraction.js new file mode 100644 index 0000000000..7d621a45cc --- /dev/null +++ b/netwerk/test/unit/test_signature_extraction.js @@ -0,0 +1,203 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests signature extraction using Windows Authenticode APIs of + * downloaded files. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Globals +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs", +}); + +const BackgroundFileSaverOutputStream = Components.Constructor( + "@mozilla.org/network/background-file-saver;1?mode=outputstream", + "nsIBackgroundFileSaver" +); + +const StringInputStream = Components.Constructor( + "@mozilla.org/io/string-input-stream;1", + "nsIStringInputStream", + "setData" +); + +const TEST_FILE_NAME_1 = "test-backgroundfilesaver-1.txt"; + +/** + * Returns a reference to a temporary file that is guaranteed not to exist and + * is cleaned up later. See FileTestUtils.getTempFile for details. + */ +function getTempFile(leafName) { + return FileTestUtils.getTempFile(leafName); +} + +/** + * Waits for the given saver object to complete. + * + * @param aSaver + * The saver, with the output stream or a stream listener implementation. + * @param aOnTargetChangeFn + * Optional callback invoked with the target file name when it changes. + * + * @return {Promise} + * @resolves When onSaveComplete is called with a success code. + * @rejects With an exception, if onSaveComplete is called with a failure code. + */ +function promiseSaverComplete(aSaver, aOnTargetChangeFn) { + return new Promise((resolve, reject) => { + aSaver.observer = { + onTargetChange: function BFSO_onSaveComplete(aSaver, aTarget) { + if (aOnTargetChangeFn) { + aOnTargetChangeFn(aTarget); + } + }, + onSaveComplete: function BFSO_onSaveComplete(aSaver, aStatus) { + if (Components.isSuccessCode(aStatus)) { + resolve(); + } else { + reject(new Components.Exception("Saver failed.", aStatus)); + } + }, + }; + }); +} + +/** + * Feeds a string to a BackgroundFileSaverOutputStream. + * + * @param aSourceString + * The source data to copy. + * @param aSaverOutputStream + * The BackgroundFileSaverOutputStream to feed. + * @param aCloseWhenDone + * If true, the output stream will be closed when the copy finishes. + * + * @return {Promise} + * @resolves When the copy completes with a success code. + * @rejects With an exception, if the copy fails. + */ +function promiseCopyToSaver(aSourceString, aSaverOutputStream, aCloseWhenDone) { + return new Promise((resolve, reject) => { + let inputStream = new StringInputStream( + aSourceString, + aSourceString.length + ); + let copier = Cc[ + "@mozilla.org/network/async-stream-copier;1" + ].createInstance(Ci.nsIAsyncStreamCopier); + copier.init( + inputStream, + aSaverOutputStream, + null, + false, + true, + 0x8000, + true, + aCloseWhenDone + ); + copier.asyncCopy( + { + onStartRequest() {}, + onStopRequest(aRequest, aContext, aStatusCode) { + if (Components.isSuccessCode(aStatusCode)) { + resolve(); + } else { + reject(new Components.Exception(aStatusCode)); + } + }, + }, + null + ); + }); +} + +var gStillRunning = true; + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +add_task(function test_setup() { + // Wait 10 minutes, that is half of the external xpcshell timeout. + do_timeout(10 * 60 * 1000, function () { + if (gStillRunning) { + do_throw("Test timed out."); + } + }); +}); + +function readFileToString(aFilename) { + let f = do_get_file(aFilename); + let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + stream.init(f, -1, 0, 0); + let buf = NetUtil.readInputStreamToString(stream, stream.available()); + return buf; +} + +add_task(async function test_signature() { + // Check that we get a signature if the saver is finished on Windows. + let destFile = getTempFile(TEST_FILE_NAME_1); + + let data = readFileToString("data/signed_win.exe"); + let saver = new BackgroundFileSaverOutputStream(); + let completionPromise = promiseSaverComplete(saver); + + try { + saver.signatureInfo; + do_throw("Can't get signature before saver is complete."); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw ex; + } + } + + saver.enableSignatureInfo(); + saver.setTarget(destFile, false); + await promiseCopyToSaver(data, saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + + // There's only one Array of certs(raw bytes) in the signature array. + Assert.equal(1, saver.signatureInfo.length); + let certLists = saver.signatureInfo; + Assert.ok(certLists.length === 1); + + // Check that it has 3 certs(raw bytes). + let certs = certLists[0]; + Assert.ok(certs.length === 3); + + const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + let signer = certDB.constructX509(certs[0]); + let issuer = certDB.constructX509(certs[1]); + let root = certDB.constructX509(certs[2]); + + let organization = "Microsoft Corporation"; + Assert.equal("Microsoft Corporation", signer.commonName); + Assert.equal(organization, signer.organization); + Assert.equal("Copyright (c) 2002 Microsoft Corp.", signer.organizationalUnit); + + Assert.equal("Microsoft Code Signing PCA", issuer.commonName); + Assert.equal(organization, issuer.organization); + Assert.equal("Copyright (c) 2000 Microsoft Corp.", issuer.organizationalUnit); + + Assert.equal("Microsoft Root Authority", root.commonName); + Assert.ok(!root.organization); + Assert.equal("Copyright (c) 1997 Microsoft Corp.", root.organizationalUnit); + + // Clean up. + destFile.remove(false); +}); + +add_task(function test_teardown() { + gStillRunning = false; +}); diff --git a/netwerk/test/unit/test_simple.js b/netwerk/test/unit/test_simple.js new file mode 100644 index 0000000000..14caa5f90c --- /dev/null +++ b/netwerk/test/unit/test_simple.js @@ -0,0 +1,68 @@ +// +// Simple HTTP test: fetches page +// + +// Note: sets Cc and Ci variables +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var testpath = "/simple"; +var httpbody = "0123456789"; + +var dbg = 0; +if (dbg) { + print("============== START =========="); +} + +function run_test() { + setup_test(); + do_test_pending(); +} + +function setup_test() { + if (dbg) { + print("============== setup_test: in"); + } + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + var channel = setupChannel(testpath); + // ChannelListener defined in head_channels.js + channel.asyncOpen(new ChannelListener(checkRequest, channel)); + if (dbg) { + print("============== setup_test: out"); + } +} + +function setupChannel(path) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + path, + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + return chan; +} + +function serverHandler(metadata, response) { + if (dbg) { + print("============== serverHandler: in"); + } + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(httpbody, httpbody.length); + if (dbg) { + print("============== serverHandler: out"); + } +} + +function checkRequest(request, data, context) { + if (dbg) { + print("============== checkRequest: in"); + } + Assert.equal(data, httpbody); + httpserver.stop(do_test_finished); + if (dbg) { + print("============== checkRequest: out"); + } +} diff --git a/netwerk/test/unit/test_sockettransportsvc_available.js b/netwerk/test/unit/test_sockettransportsvc_available.js new file mode 100644 index 0000000000..664b6a853d --- /dev/null +++ b/netwerk/test/unit/test_sockettransportsvc_available.js @@ -0,0 +1,11 @@ +"use strict"; + +function run_test() { + try { + var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( + Ci.nsISocketTransportService + ); + } catch (e) {} + + Assert.ok(!!sts); +} diff --git a/netwerk/test/unit/test_socks.js b/netwerk/test/unit/test_socks.js new file mode 100644 index 0000000000..6ce9f4895e --- /dev/null +++ b/netwerk/test/unit/test_socks.js @@ -0,0 +1,520 @@ +"use strict"; + +var CC = Components.Constructor; + +const ServerSocket = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init" +); +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +const DirectoryService = CC( + "@mozilla.org/file/directory_service;1", + "nsIProperties" +); +const Process = CC("@mozilla.org/process/util;1", "nsIProcess", "init"); + +const currentThread = + Cc["@mozilla.org/thread-manager;1"].getService().currentThread; + +var socks_test_server = null; +var socks_listen_port = -1; + +function getAvailableBytes(input) { + var len = 0; + + try { + len = input.available(); + } catch (e) {} + + return len; +} + +function runScriptSubprocess(script, args) { + var ds = new DirectoryService(); + var bin = ds.get("XREExeF", Ci.nsIFile); + if (!bin.exists()) { + do_throw("Can't find xpcshell binary"); + } + + var file = do_get_file(script); + var proc = new Process(bin); + var procArgs = [file.path].concat(args); + + proc.run(false, procArgs, procArgs.length); + + return proc; +} + +function buf2ip(buf) { + if (buf.length == 16) { + var ip = + ((buf[0] << 4) | buf[1]).toString(16) + + ":" + + ((buf[2] << 4) | buf[3]).toString(16) + + ":" + + ((buf[4] << 4) | buf[5]).toString(16) + + ":" + + ((buf[6] << 4) | buf[7]).toString(16) + + ":" + + ((buf[8] << 4) | buf[9]).toString(16) + + ":" + + ((buf[10] << 4) | buf[11]).toString(16) + + ":" + + ((buf[12] << 4) | buf[13]).toString(16) + + ":" + + ((buf[14] << 4) | buf[15]).toString(16); + for (var i = 8; i >= 2; i--) { + var re = new RegExp("(^|:)(0(:|$)){" + i + "}"); + var shortip = ip.replace(re, "::"); + if (shortip != ip) { + return shortip; + } + } + return ip; + } + return buf.join("."); +} + +function buf2int(buf) { + var n = 0; + + for (var i in buf) { + n |= buf[i] << ((buf.length - i - 1) * 8); + } + + return n; +} + +function buf2str(buf) { + return String.fromCharCode.apply(null, buf); +} + +const STATE_WAIT_GREETING = 1; +const STATE_WAIT_SOCKS4_REQUEST = 2; +const STATE_WAIT_SOCKS4_USERNAME = 3; +const STATE_WAIT_SOCKS4_HOSTNAME = 4; +const STATE_WAIT_SOCKS5_GREETING = 5; +const STATE_WAIT_SOCKS5_REQUEST = 6; +const STATE_WAIT_PONG = 7; +const STATE_GOT_PONG = 8; + +function SocksClient(server, client_in, client_out) { + this.server = server; + this.type = ""; + this.username = ""; + this.dest_name = ""; + this.dest_addr = []; + this.dest_port = []; + + this.client_in = client_in; + this.client_out = client_out; + this.inbuf = []; + this.outbuf = String(); + this.state = STATE_WAIT_GREETING; + this.waitRead(this.client_in); +} +SocksClient.prototype = { + onInputStreamReady(input) { + var len = getAvailableBytes(input); + + if (len == 0) { + print("server: client closed!"); + Assert.equal(this.state, STATE_GOT_PONG); + this.close(); + this.server.testCompleted(this); + return; + } + + var bin = new BinaryInputStream(input); + var data = bin.readByteArray(len); + this.inbuf = this.inbuf.concat(data); + + switch (this.state) { + case STATE_WAIT_GREETING: + this.checkSocksGreeting(); + break; + case STATE_WAIT_SOCKS4_REQUEST: + this.checkSocks4Request(); + break; + case STATE_WAIT_SOCKS4_USERNAME: + this.checkSocks4Username(); + break; + case STATE_WAIT_SOCKS4_HOSTNAME: + this.checkSocks4Hostname(); + break; + case STATE_WAIT_SOCKS5_GREETING: + this.checkSocks5Greeting(); + break; + case STATE_WAIT_SOCKS5_REQUEST: + this.checkSocks5Request(); + break; + case STATE_WAIT_PONG: + this.checkPong(); + break; + default: + do_throw("server: read in invalid state!"); + } + + this.waitRead(input); + }, + + onOutputStreamReady(output) { + var len = output.write(this.outbuf, this.outbuf.length); + if (len != this.outbuf.length) { + this.outbuf = this.outbuf.substring(len); + this.waitWrite(output); + } else { + this.outbuf = String(); + } + }, + + waitRead(input) { + input.asyncWait(this, 0, 0, currentThread); + }, + + waitWrite(output) { + output.asyncWait(this, 0, 0, currentThread); + }, + + write(buf) { + this.outbuf += buf; + this.waitWrite(this.client_out); + }, + + checkSocksGreeting() { + if (!this.inbuf.length) { + return; + } + + if (this.inbuf[0] == 4) { + print("server: got socks 4"); + this.type = "socks4"; + this.state = STATE_WAIT_SOCKS4_REQUEST; + this.checkSocks4Request(); + } else if (this.inbuf[0] == 5) { + print("server: got socks 5"); + this.type = "socks"; + this.state = STATE_WAIT_SOCKS5_GREETING; + this.checkSocks5Greeting(); + } else { + do_throw("Unknown socks protocol!"); + } + }, + + checkSocks4Request() { + if (this.inbuf.length < 8) { + return; + } + + Assert.equal(this.inbuf[1], 0x01); + + this.dest_port = this.inbuf.slice(2, 4); + this.dest_addr = this.inbuf.slice(4, 8); + + this.inbuf = this.inbuf.slice(8); + this.state = STATE_WAIT_SOCKS4_USERNAME; + this.checkSocks4Username(); + }, + + readString() { + var i = this.inbuf.indexOf(0); + var str = null; + + if (i >= 0) { + var buf = this.inbuf.slice(0, i); + str = buf2str(buf); + this.inbuf = this.inbuf.slice(i + 1); + } + + return str; + }, + + checkSocks4Username() { + var str = this.readString(); + + if (str == null) { + return; + } + + this.username = str; + if ( + this.dest_addr[0] == 0 && + this.dest_addr[1] == 0 && + this.dest_addr[2] == 0 && + this.dest_addr[3] != 0 + ) { + this.state = STATE_WAIT_SOCKS4_HOSTNAME; + this.checkSocks4Hostname(); + } else { + this.sendSocks4Response(); + } + }, + + checkSocks4Hostname() { + var str = this.readString(); + + if (str == null) { + return; + } + + this.dest_name = str; + this.sendSocks4Response(); + }, + + sendSocks4Response() { + this.outbuf = "\x00\x5a\x00\x00\x00\x00\x00\x00"; + this.sendPing(); + }, + + checkSocks5Greeting() { + if (this.inbuf.length < 2) { + return; + } + var nmethods = this.inbuf[1]; + if (this.inbuf.length < 2 + nmethods) { + return; + } + + Assert.ok(nmethods >= 1); + var methods = this.inbuf.slice(2, 2 + nmethods); + Assert.ok(0 in methods); + + this.inbuf = []; + this.state = STATE_WAIT_SOCKS5_REQUEST; + this.write("\x05\x00"); + }, + + checkSocks5Request() { + if (this.inbuf.length < 4) { + return; + } + + Assert.equal(this.inbuf[0], 0x05); + Assert.equal(this.inbuf[1], 0x01); + Assert.equal(this.inbuf[2], 0x00); + + var atype = this.inbuf[3]; + var len; + var name = false; + + switch (atype) { + case 0x01: + len = 4; + break; + case 0x03: + len = this.inbuf[4]; + name = true; + break; + case 0x04: + len = 16; + break; + default: + do_throw("Unknown address type " + atype); + } + + if (name) { + if (this.inbuf.length < 4 + len + 1 + 2) { + return; + } + + let buf = this.inbuf.slice(5, 5 + len); + this.dest_name = buf2str(buf); + len += 1; + } else { + if (this.inbuf.length < 4 + len + 2) { + return; + } + + this.dest_addr = this.inbuf.slice(4, 4 + len); + } + + len += 4; + this.dest_port = this.inbuf.slice(len, len + 2); + this.inbuf = this.inbuf.slice(len + 2); + this.sendSocks5Response(); + }, + + sendSocks5Response() { + if (this.dest_addr.length == 16) { + // send a successful response with the address, [::1]:80 + this.outbuf += + "\x05\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x80"; + } else { + // send a successful response with the address, 127.0.0.1:80 + this.outbuf += "\x05\x00\x00\x01\x7f\x00\x00\x01\x00\x80"; + } + this.sendPing(); + }, + + sendPing() { + print("server: sending ping"); + this.state = STATE_WAIT_PONG; + this.outbuf += "PING!"; + this.inbuf = []; + this.waitWrite(this.client_out); + }, + + checkPong() { + var pong = buf2str(this.inbuf); + Assert.equal(pong, "PONG!"); + this.state = STATE_GOT_PONG; + }, + + close() { + this.client_in.close(); + this.client_out.close(); + }, +}; + +function SocksTestServer() { + this.listener = ServerSocket(-1, true, -1); + socks_listen_port = this.listener.port; + print("server: listening on", socks_listen_port); + this.listener.asyncListen(this); + this.test_cases = []; + this.client_connections = []; + this.client_subprocess = null; + // port is used as the ID for test cases + this.test_port_id = 8000; + this.tests_completed = 0; +} +SocksTestServer.prototype = { + addTestCase(test) { + test.finished = false; + test.port = this.test_port_id++; + this.test_cases.push(test); + }, + + pickTest(id) { + for (var i in this.test_cases) { + var test = this.test_cases[i]; + if (test.port == id) { + this.tests_completed++; + return test; + } + } + do_throw("No test case with id " + id); + return null; + }, + + testCompleted(client) { + var port_id = buf2int(client.dest_port); + var test = this.pickTest(port_id); + + print("server: test finished", test.port); + Assert.ok(test != null); + Assert.equal(test.expectedType || test.type, client.type); + Assert.equal(test.port, port_id); + + if (test.remote_dns) { + Assert.equal(test.host, client.dest_name); + } else { + Assert.equal(test.host, buf2ip(client.dest_addr)); + } + + if (this.test_cases.length == this.tests_completed) { + print("server: all tests completed"); + this.close(); + do_test_finished(); + } + }, + + runClientSubprocess() { + var argv = []; + + // marshaled: socks_ver|server_port|dest_host|dest_port|<remote|local> + for (var test of this.test_cases) { + var arg = + test.type + + "|" + + String(socks_listen_port) + + "|" + + test.host + + "|" + + test.port + + "|"; + if (test.remote_dns) { + arg += "remote"; + } else { + arg += "local"; + } + print("server: using test case", arg); + argv.push(arg); + } + + this.client_subprocess = runScriptSubprocess( + "socks_client_subprocess.js", + argv + ); + }, + + onSocketAccepted(socket, trans) { + print("server: got client connection"); + var input = trans.openInputStream(0, 0, 0); + var output = trans.openOutputStream(0, 0, 0); + var client = new SocksClient(this, input, output); + this.client_connections.push(client); + }, + + onStopListening(socket) {}, + + close() { + if (this.client_subprocess) { + try { + this.client_subprocess.kill(); + } catch (x) { + do_note_exception(x, "Killing subprocess failed"); + } + this.client_subprocess = null; + } + this.client_connections = []; + if (this.listener) { + this.listener.close(); + this.listener = null; + } + }, +}; + +function run_test() { + socks_test_server = new SocksTestServer(); + + socks_test_server.addTestCase({ + type: "socks4", + host: "127.0.0.1", + remote_dns: false, + }); + socks_test_server.addTestCase({ + type: "socks4", + host: "12345.xxx", + remote_dns: true, + }); + socks_test_server.addTestCase({ + type: "socks4", + expectedType: "socks", + host: "::1", + remote_dns: false, + }); + socks_test_server.addTestCase({ + type: "socks", + host: "127.0.0.1", + remote_dns: false, + }); + socks_test_server.addTestCase({ + type: "socks", + host: "abcdefg.xxx", + remote_dns: true, + }); + socks_test_server.addTestCase({ + type: "socks", + host: "::1", + remote_dns: false, + }); + socks_test_server.runClientSubprocess(); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_speculative_connect.js b/netwerk/test/unit/test_speculative_connect.js new file mode 100644 index 0000000000..8dd594afb6 --- /dev/null +++ b/netwerk/test/unit/test_speculative_connect.js @@ -0,0 +1,362 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* vim: set ts=4 sts=4 et sw=4 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"; + +var CC = Components.Constructor; +const ServerSocket = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init" +); +var serv; +var ios; + +/** Example local IP addresses (literal IP address hostname). + * + * Note: for IPv6 Unique Local and Link Local, a wider range of addresses is + * set aside than those most commonly used. Technically, link local addresses + * include those beginning with fe80:: through febf::, although in practise + * only fe80:: is used. Necko code blocks speculative connections for the wider + * range; hence, this test considers that range too. + */ +var localIPv4Literals = [ + // IPv4 RFC1918 \ + "10.0.0.1", + "10.10.10.10", + "10.255.255.255", // 10/8 + "172.16.0.1", + "172.23.172.12", + "172.31.255.255", // 172.16/20 + "192.168.0.1", + "192.168.192.168", + "192.168.255.255", // 192.168/16 + // IPv4 Link Local + "169.254.0.1", + "169.254.192.154", + "169.254.255.255", // 169.254/16 +]; +var localIPv6Literals = [ + // IPv6 Unique Local fc00::/7 + "fc00::1", + "fdfe:dcba:9876:abcd:ef01:2345:6789:abcd", + "fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + // IPv6 Link Local fe80::/10 + "fe80::1", + "fe80::abcd:ef01:2345:6789", + "febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff", +]; +var localIPLiterals = localIPv4Literals.concat(localIPv6Literals); + +/** Test function list and descriptions. + */ +var testList = [ + test_speculative_connect, + test_hostnames_resolving_to_local_addresses, + test_proxies_with_local_addresses, +]; + +var testDescription = [ + "Expect pass with localhost", + "Expect failure with resolved local IPs", + "Expect failure for proxies with local IPs", +]; + +var testIdx = 0; +var hostIdx = 0; + +/** TestServer + * + * Implements nsIServerSocket for test_speculative_connect. + */ +function TestServer() { + this.listener = ServerSocket(-1, true, -1); + this.listener.asyncListen(this); +} + +TestServer.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIServerSocket"]), + onSocketAccepted(socket, trans) { + try { + this.listener.close(); + } catch (e) {} + Assert.ok(true); + next_test(); + }, + + onStopListening(socket) {}, +}; + +/** TestFailedStreamCallback + * + * Implements nsI[Input|Output]StreamCallback for socket layer tests. + * Expect failure in all cases + */ +function TestFailedStreamCallback(transport, hostname, next) { + this.transport = transport; + this.hostname = hostname; + this.next = next; + this.dummyContent = "G"; + this.closed = false; +} + +TestFailedStreamCallback.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInputStreamCallback", + "nsIOutputStreamCallback", + ]), + processException(e) { + if (this.closed) { + return; + } + do_check_instanceof(e, Ci.nsIException); + // A refusal to connect speculatively should throw an error. + Assert.equal(e.result, Cr.NS_ERROR_CONNECTION_REFUSED); + this.closed = true; + this.transport.close(Cr.NS_BINDING_ABORTED); + this.next(); + }, + onOutputStreamReady(outstream) { + info("outputstream handler."); + Assert.notEqual(typeof outstream, undefined); + try { + outstream.write(this.dummyContent, this.dummyContent.length); + } catch (e) { + this.processException(e); + return; + } + info("no exception on write. Wait for read."); + }, + onInputStreamReady(instream) { + info("inputstream handler."); + Assert.notEqual(typeof instream, undefined); + try { + instream.available(); + } catch (e) { + this.processException(e); + return; + } + do_throw("Speculative Connect should have failed for " + this.hostname); + this.transport.close(Cr.NS_BINDING_ABORTED); + this.next(); + }, +}; + +/** test_speculative_connect + * + * Tests a basic positive case using nsIOService.SpeculativeConnect: + * connecting to localhost. + */ +function test_speculative_connect() { + serv = new TestServer(); + var ssm = Services.scriptSecurityManager; + var URI = ios.newURI( + "http://localhost:" + serv.listener.port + "/just/a/test" + ); + var principal = ssm.createContentPrincipal(URI, {}); + + ios + .QueryInterface(Ci.nsISpeculativeConnect) + .speculativeConnect(URI, principal, null, false); +} + +/* Speculative connections should not be allowed for hosts with local IP + * addresses (Bug 853423). That list includes: + * -- IPv4 RFC1918 and Link Local Addresses. + * -- IPv6 Unique and Link Local Addresses. + * + * Two tests are required: + * 1. Verify IP Literals passed to the SpeculativeConnect API. + * 2. Verify hostnames that need to be resolved at the socket layer. + */ + +/** test_hostnames_resolving_to_addresses + * + * Common test function for resolved hostnames. Takes a list of hosts, a + * boolean to determine if the test is expected to succeed or fail, and a + * function to call the next test case. + */ +function test_hostnames_resolving_to_addresses(host, next) { + info(host); + var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( + Ci.nsISocketTransportService + ); + Assert.notEqual(typeof sts, undefined); + var transport = sts.createTransport([], host, 80, null, null); + Assert.notEqual(typeof transport, undefined); + + transport.connectionFlags = Ci.nsISocketTransport.DISABLE_RFC1918; + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 1); + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, 1); + Assert.equal(1, transport.getTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT)); + + var outStream = transport.openOutputStream( + Ci.nsITransport.OPEN_UNBUFFERED, + 0, + 0 + ); + var inStream = transport.openInputStream(0, 0, 0); + Assert.notEqual(typeof outStream, undefined); + Assert.notEqual(typeof inStream, undefined); + + var callback = new TestFailedStreamCallback(transport, host, next); + Assert.notEqual(typeof callback, undefined); + + // Need to get main thread pointer to ensure nsSocketTransport::AsyncWait + // adds callback to ns*StreamReadyEvent on main thread, and doesn't + // addref off the main thread. + var gThreadManager = Services.tm; + var mainThread = gThreadManager.currentThread; + + try { + outStream + .QueryInterface(Ci.nsIAsyncOutputStream) + .asyncWait(callback, 0, 0, mainThread); + inStream + .QueryInterface(Ci.nsIAsyncInputStream) + .asyncWait(callback, 0, 0, mainThread); + } catch (e) { + do_throw("asyncWait should not fail!"); + } +} + +/** + * test_hostnames_resolving_to_local_addresses + * + * Creates an nsISocketTransport and simulates a speculative connect request + * for a hostname that resolves to a local IP address. + * Runs asynchronously; on test success (i.e. failure to connect), the callback + * will call this function again until all hostnames in the test list are done. + * + * Note: This test also uses an IP literal for the hostname. This should be ok, + * as the socket layer will ask for the hostname to be resolved anyway, and DNS + * code should return a numerical version of the address internally. + */ +function test_hostnames_resolving_to_local_addresses() { + if (hostIdx >= localIPLiterals.length) { + // No more local IP addresses; move on. + next_test(); + return; + } + var host = localIPLiterals[hostIdx++]; + // Test another local IP address when the current one is done. + var next = test_hostnames_resolving_to_local_addresses; + test_hostnames_resolving_to_addresses(host, next); +} + +/** test_speculative_connect_with_host_list + * + * Common test function for resolved proxy hosts. Takes a list of hosts, a + * boolean to determine if the test is expected to succeed or fail, and a + * function to call the next test case. + */ +function test_proxies(proxyHost, next) { + info("Proxy: " + proxyHost); + var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( + Ci.nsISocketTransportService + ); + Assert.notEqual(typeof sts, undefined); + var pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + Assert.notEqual(typeof pps, undefined); + + var proxyInfo = pps.newProxyInfo("http", proxyHost, 8080, "", "", 0, 1, null); + Assert.notEqual(typeof proxyInfo, undefined); + + var transport = sts.createTransport([], "dummyHost", 80, proxyInfo, null); + Assert.notEqual(typeof transport, undefined); + + transport.connectionFlags = Ci.nsISocketTransport.DISABLE_RFC1918; + + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 1); + Assert.equal(1, transport.getTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT)); + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, 1); + + var outStream = transport.openOutputStream( + Ci.nsITransport.OPEN_UNBUFFERED, + 0, + 0 + ); + var inStream = transport.openInputStream(0, 0, 0); + Assert.notEqual(typeof outStream, undefined); + Assert.notEqual(typeof inStream, undefined); + + var callback = new TestFailedStreamCallback(transport, proxyHost, next); + Assert.notEqual(typeof callback, undefined); + + // Need to get main thread pointer to ensure nsSocketTransport::AsyncWait + // adds callback to ns*StreamReadyEvent on main thread, and doesn't + // addref off the main thread. + var gThreadManager = Services.tm; + var mainThread = gThreadManager.currentThread; + + try { + outStream + .QueryInterface(Ci.nsIAsyncOutputStream) + .asyncWait(callback, 0, 0, mainThread); + inStream + .QueryInterface(Ci.nsIAsyncInputStream) + .asyncWait(callback, 0, 0, mainThread); + } catch (e) { + do_throw("asyncWait should not fail!"); + } +} + +/** + * test_proxies_with_local_addresses + * + * Creates an nsISocketTransport and simulates a speculative connect request + * for a proxy that resolves to a local IP address. + * Runs asynchronously; on test success (i.e. failure to connect), the callback + * will call this function again until all proxies in the test list are done. + * + * Note: This test also uses an IP literal for the proxy. This should be ok, + * as the socket layer will ask for the proxy to be resolved anyway, and DNS + * code should return a numerical version of the address internally. + */ +function test_proxies_with_local_addresses() { + if (hostIdx >= localIPLiterals.length) { + // No more local IP addresses; move on. + next_test(); + return; + } + var host = localIPLiterals[hostIdx++]; + // Test another local IP address when the current one is done. + var next = test_proxies_with_local_addresses; + test_proxies(host, next); +} + +/** next_test + * + * Calls the next test in testList. Each test is responsible for calling this + * function when its test cases are complete. + */ +function next_test() { + if (testIdx >= testList.length) { + // No more tests; we're done. + do_test_finished(); + return; + } + info("SpeculativeConnect: " + testDescription[testIdx]); + hostIdx = 0; + // Start next test in list. + testList[testIdx++](); +} + +/** run_test + * + * Main entry function for test execution. + */ +function run_test() { + ios = Services.io; + + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); + }); + + do_test_pending(); + next_test(); +} diff --git a/netwerk/test/unit/test_stale-while-revalidate_loop.js b/netwerk/test/unit/test_stale-while-revalidate_loop.js new file mode 100644 index 0000000000..dc28815119 --- /dev/null +++ b/netwerk/test/unit/test_stale-while-revalidate_loop.js @@ -0,0 +1,43 @@ +/* + +Tests the Cache-control: stale-while-revalidate response directive. + +Loads a HTTPS resource with the stale-while-revalidate and tries to load it +twice. + +*/ + +"use strict"; + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +async function get_response(channel, fromCache) { + return new Promise(resolve => { + channel.asyncOpen( + new ChannelListener((request, buffer, ctx, isFromCache) => { + resolve(buffer); + }) + ); + }); +} + +add_task(async function () { + do_get_profile(); + const PORT = Services.env.get("MOZHTTP2_PORT"); + const URI = `https://localhost:${PORT}/stale-while-revalidate-loop-test`; + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + let response = await get_response(make_channel(URI), false); + ok(response == "1", "got response ver 1"); + response = await get_response(make_channel(URI), false); + ok(response == "1", "got response ver 1"); +}); diff --git a/netwerk/test/unit/test_stale-while-revalidate_max-age-0.js b/netwerk/test/unit/test_stale-while-revalidate_max-age-0.js new file mode 100644 index 0000000000..0c89d237a6 --- /dev/null +++ b/netwerk/test/unit/test_stale-while-revalidate_max-age-0.js @@ -0,0 +1,111 @@ +/* + +Tests the Cache-control: stale-while-revalidate response directive. + +Purpose is to check we perform the background revalidation when max-age=0 but +the window is set and we hit it. + +* Make request #1. + - response is from the server and version=1 + - max-age=0, stale-while-revalidate=9999 +* Switch version of the data on the server and prolong the max-age to not let req #3 + do a bck reval at the end of the test (prevent leaks/shutdown races.) +* Make request #2 in 2 seconds (entry should be expired by that time, but fall into + the reval window.) + - response is from the cache, version=1 + - a new background request should be made for the data +* Wait for "http-on-background-revalidation" notifying finish of the background reval. +* Make request #3. + - response is from the cache, version=2 +* Done. + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +let max_age; +let version; +let generate_response = ver => `response version=${ver}`; + +function test_handler(metadata, response) { + const originalBody = generate_response(version); + response.setHeader("Content-Type", "text/html", false); + response.setHeader( + "Cache-control", + `max-age=${max_age}, stale-while-revalidate=9999`, + false + ); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(originalBody, originalBody.length); +} + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +async function get_response(channel, fromCache) { + return new Promise(resolve => { + channel.asyncOpen( + new ChannelListener((request, buffer, ctx, isFromCache) => { + ok(fromCache == isFromCache, `got response from cache = ${fromCache}`); + resolve(buffer); + }) + ); + }); +} + +async function sleep(time) { + return new Promise(resolve => { + do_timeout(time * 1000, resolve); + }); +} + +async function stop_server(httpserver) { + return new Promise(resolve => { + httpserver.stop(resolve); + }); +} + +async function background_reval_promise() { + return new Promise(resolve => { + Services.obs.addObserver(resolve, "http-on-background-revalidation"); + }); +} + +add_task(async function () { + let httpserver = new HttpServer(); + httpserver.registerPathHandler("/testdir", test_handler); + httpserver.start(-1); + const PORT = httpserver.identity.primaryPort; + const URI = `http://localhost:${PORT}/testdir`; + + let response; + + version = 1; + max_age = 0; + response = await get_response(make_channel(URI), false); + ok(response == generate_response(1), "got response ver 1"); + + await sleep(2); + + // must specifically wait for the internal channel to finish the reval to make + // the test race-free. + let reval_done = background_reval_promise(); + + version = 2; + max_age = 100; + response = await get_response(make_channel(URI), true); + ok(response == generate_response(1), "got response ver 1"); + + await reval_done; + + response = await get_response(make_channel(URI), true); + ok(response == generate_response(2), "got response ver 2"); + + await stop_server(httpserver); +}); diff --git a/netwerk/test/unit/test_stale-while-revalidate_negative.js b/netwerk/test/unit/test_stale-while-revalidate_negative.js new file mode 100644 index 0000000000..51648a0a9b --- /dev/null +++ b/netwerk/test/unit/test_stale-while-revalidate_negative.js @@ -0,0 +1,90 @@ +/* + +Tests the Cache-control: stale-while-revalidate response directive. + +Purpose is to check we DON'T perform the background revalidation when we make the +request past the reval window. + +* Make request #1. + - response is from the server and version=1 + - max-age=1, stale-while-revalidate=1 +* Switch version of the data on the server. +* Make request #2 in 3 seconds (entry should be expired by that time and no longer + fall into the reval window.) + - response is from the server, version=2 +* Done. + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +let max_age; +let version; +let generate_response = ver => `response version=${ver}`; + +function test_handler(metadata, response) { + const originalBody = generate_response(version); + response.setHeader("Content-Type", "text/html", false); + response.setHeader( + "Cache-control", + `max-age=${max_age}, stale-while-revalidate=1`, + false + ); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(originalBody, originalBody.length); +} + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +async function get_response(channel, fromCache) { + return new Promise(resolve => { + channel.asyncOpen( + new ChannelListener((request, buffer, ctx, isFromCache) => { + ok(fromCache == isFromCache, `got response from cache = ${fromCache}`); + resolve(buffer); + }) + ); + }); +} + +async function sleep(time) { + return new Promise(resolve => { + do_timeout(time * 1000, resolve); + }); +} + +async function stop_server(httpserver) { + return new Promise(resolve => { + httpserver.stop(resolve); + }); +} + +add_task(async function () { + let httpserver = new HttpServer(); + httpserver.registerPathHandler("/testdir", test_handler); + httpserver.start(-1); + const PORT = httpserver.identity.primaryPort; + const URI = `http://localhost:${PORT}/testdir`; + + let response; + + version = 1; + max_age = 1; + response = await get_response(make_channel(URI), false); + ok(response == generate_response(1), "got response ver 1"); + + await sleep(max_age + 1 /* stale window */ + 1 /* to expire the window */); + + version = 2; + response = await get_response(make_channel(URI), false); + ok(response == generate_response(2), "got response ver 2"); + + await stop_server(httpserver); +}); diff --git a/netwerk/test/unit/test_stale-while-revalidate_positive.js b/netwerk/test/unit/test_stale-while-revalidate_positive.js new file mode 100644 index 0000000000..e13aa578f8 --- /dev/null +++ b/netwerk/test/unit/test_stale-while-revalidate_positive.js @@ -0,0 +1,111 @@ +/* + +Tests the Cache-control: stale-while-revalidate response directive. + +Purpose is to check we perform the background revalidation when the window is set +and we hit it. + +* Make request #1. + - response is from the server and version=1 + - max-age=1, stale-while-revalidate=9999 +* Switch version of the data on the server and prolong the max-age to not let req #3 + do a bck reval at the end of the test (prevent leaks/shutdown races.) +* Make request #2 in 2 seconds (entry should be expired by that time, but fall into + the reval window.) + - response is from the cache, version=1 + - a new background request should be made for the data +* Wait for "http-on-background-revalidation" notifying finish of the background reval. +* Make request #3. + - response is from the cache, version=2 +* Done. + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +let max_age; +let version; +let generate_response = ver => `response version=${ver}`; + +function test_handler(metadata, response) { + const originalBody = generate_response(version); + response.setHeader("Content-Type", "text/html", false); + response.setHeader( + "Cache-control", + `max-age=${max_age}, stale-while-revalidate=9999`, + false + ); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(originalBody, originalBody.length); +} + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +async function get_response(channel, fromCache) { + return new Promise(resolve => { + channel.asyncOpen( + new ChannelListener((request, buffer, ctx, isFromCache) => { + ok(fromCache == isFromCache, `got response from cache = ${fromCache}`); + resolve(buffer); + }) + ); + }); +} + +async function sleep(time) { + return new Promise(resolve => { + do_timeout(time * 1000, resolve); + }); +} + +async function stop_server(httpserver) { + return new Promise(resolve => { + httpserver.stop(resolve); + }); +} + +async function background_reval_promise() { + return new Promise(resolve => { + Services.obs.addObserver(resolve, "http-on-background-revalidation"); + }); +} + +add_task(async function () { + let httpserver = new HttpServer(); + httpserver.registerPathHandler("/testdir", test_handler); + httpserver.start(-1); + const PORT = httpserver.identity.primaryPort; + const URI = `http://localhost:${PORT}/testdir`; + + let response; + + version = 1; + max_age = 1; + response = await get_response(make_channel(URI), false); + ok(response == generate_response(1), "got response ver 1"); + + await sleep(max_age + 1); + + // must specifically wait for the internal channel to finish the reval to make + // the test race-free. + let reval_done = background_reval_promise(); + + version = 2; + max_age = 100; + response = await get_response(make_channel(URI), true); + ok(response == generate_response(1), "got response ver 1"); + + await reval_done; + + response = await get_response(make_channel(URI), true); + ok(response == generate_response(2), "got response ver 2"); + + await stop_server(httpserver); +}); diff --git a/netwerk/test/unit/test_standardurl.js b/netwerk/test/unit/test_standardurl.js new file mode 100644 index 0000000000..905784a54c --- /dev/null +++ b/netwerk/test/unit/test_standardurl.js @@ -0,0 +1,1057 @@ +"use strict"; + +const gPrefs = Services.prefs; + +function symmetricEquality(expect, a, b) { + /* Use if/else instead of |do_check_eq(expect, a.spec == b.spec)| so + that we get the specs output on the console if the check fails. + */ + if (expect) { + /* Check all the sub-pieces too, since that can help with + debugging cases when equals() returns something unexpected */ + /* We don't check port in the loop, because it can be defaulted in + some cases. */ + [ + "spec", + "prePath", + "scheme", + "userPass", + "username", + "password", + "hostPort", + "host", + "pathQueryRef", + "filePath", + "query", + "ref", + "directory", + "fileName", + "fileBaseName", + "fileExtension", + ].map(function (prop) { + dump("Testing '" + prop + "'\n"); + Assert.equal(a[prop], b[prop]); + }); + } else { + Assert.notEqual(a.spec, b.spec); + } + Assert.equal(expect, a.equals(b)); + Assert.equal(expect, b.equals(a)); +} + +function stringToURL(str) { + return Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIStandardURLMutator) + .init(Ci.nsIStandardURL.URLTYPE_AUTHORITY, 80, str, "UTF-8", null) + .finalize() + .QueryInterface(Ci.nsIURL); +} + +function pairToURLs(pair) { + Assert.equal(pair.length, 2); + return pair.map(stringToURL); +} + +add_test(function test_setEmptyPath() { + var pairs = [ + ["http://example.com", "http://example.com/tests/dom/tests"], + ["http://example.com:80", "http://example.com/tests/dom/tests"], + ["http://example.com:80/", "http://example.com/tests/dom/test"], + ["http://example.com/", "http://example.com/tests/dom/tests"], + ["http://example.com/a", "http://example.com/tests/dom/tests"], + ["http://example.com:80/a", "http://example.com/tests/dom/tests"], + ].map(pairToURLs); + + for (var [provided, target] of pairs) { + symmetricEquality(false, target, provided); + + provided = provided.mutate().setPathQueryRef("").finalize(); + target = target.mutate().setPathQueryRef("").finalize(); + + Assert.equal(provided.spec, target.spec); + symmetricEquality(true, target, provided); + } + run_next_test(); +}); + +add_test(function test_setQuery() { + var pairs = [ + ["http://example.com", "http://example.com/?foo"], + ["http://example.com/bar", "http://example.com/bar?foo"], + ["http://example.com#bar", "http://example.com/?foo#bar"], + ["http://example.com/#bar", "http://example.com/?foo#bar"], + ["http://example.com/?longerthanfoo#bar", "http://example.com/?foo#bar"], + ["http://example.com/?longerthanfoo", "http://example.com/?foo"], + /* And one that's nonempty but shorter than "foo" */ + ["http://example.com/?f#bar", "http://example.com/?foo#bar"], + ["http://example.com/?f", "http://example.com/?foo"], + ].map(pairToURLs); + + for (var [provided, target] of pairs) { + symmetricEquality(false, provided, target); + + provided = provided + .mutate() + .setQuery("foo") + .finalize() + .QueryInterface(Ci.nsIURL); + + Assert.equal(provided.spec, target.spec); + symmetricEquality(true, provided, target); + } + + [provided, target] = [ + "http://example.com/#", + "http://example.com/?foo#bar", + ].map(stringToURL); + symmetricEquality(false, provided, target); + provided = provided + .mutate() + .setQuery("foo") + .finalize() + .QueryInterface(Ci.nsIURL); + symmetricEquality(false, provided, target); + + var newProvided = Services.io + .newURI("#bar", null, provided) + .QueryInterface(Ci.nsIURL); + + Assert.equal(newProvided.spec, target.spec); + symmetricEquality(true, newProvided, target); + run_next_test(); +}); + +add_test(function test_setRef() { + var tests = [ + ["http://example.com", "", "http://example.com/"], + ["http://example.com:80", "", "http://example.com:80/"], + ["http://example.com:80/", "", "http://example.com:80/"], + ["http://example.com/", "", "http://example.com/"], + ["http://example.com/a", "", "http://example.com/a"], + ["http://example.com:80/a", "", "http://example.com:80/a"], + + ["http://example.com", "x", "http://example.com/#x"], + ["http://example.com:80", "x", "http://example.com:80/#x"], + ["http://example.com:80/", "x", "http://example.com:80/#x"], + ["http://example.com/", "x", "http://example.com/#x"], + ["http://example.com/a", "x", "http://example.com/a#x"], + ["http://example.com:80/a", "x", "http://example.com:80/a#x"], + + ["http://example.com", "xx", "http://example.com/#xx"], + ["http://example.com:80", "xx", "http://example.com:80/#xx"], + ["http://example.com:80/", "xx", "http://example.com:80/#xx"], + ["http://example.com/", "xx", "http://example.com/#xx"], + ["http://example.com/a", "xx", "http://example.com/a#xx"], + ["http://example.com:80/a", "xx", "http://example.com:80/a#xx"], + + [ + "http://example.com", + "xxxxxxxxxxxxxx", + "http://example.com/#xxxxxxxxxxxxxx", + ], + [ + "http://example.com:80", + "xxxxxxxxxxxxxx", + "http://example.com:80/#xxxxxxxxxxxxxx", + ], + [ + "http://example.com:80/", + "xxxxxxxxxxxxxx", + "http://example.com:80/#xxxxxxxxxxxxxx", + ], + [ + "http://example.com/", + "xxxxxxxxxxxxxx", + "http://example.com/#xxxxxxxxxxxxxx", + ], + [ + "http://example.com/a", + "xxxxxxxxxxxxxx", + "http://example.com/a#xxxxxxxxxxxxxx", + ], + [ + "http://example.com:80/a", + "xxxxxxxxxxxxxx", + "http://example.com:80/a#xxxxxxxxxxxxxx", + ], + ]; + + for (var [before, ref, result] of tests) { + /* Test1: starting with empty ref */ + var a = stringToURL(before); + a = a.mutate().setRef(ref).finalize().QueryInterface(Ci.nsIURL); + var b = stringToURL(result); + + Assert.equal(a.spec, b.spec); + Assert.equal(ref, b.ref); + symmetricEquality(true, a, b); + + /* Test2: starting with non-empty */ + a = a.mutate().setRef("yyyy").finalize().QueryInterface(Ci.nsIURL); + var c = stringToURL(before); + c = c.mutate().setRef("yyyy").finalize().QueryInterface(Ci.nsIURL); + symmetricEquality(true, a, c); + + /* Test3: reset the ref */ + a = a.mutate().setRef("").finalize().QueryInterface(Ci.nsIURL); + symmetricEquality(true, a, stringToURL(before)); + + /* Test4: verify again after reset */ + a = a.mutate().setRef(ref).finalize().QueryInterface(Ci.nsIURL); + symmetricEquality(true, a, b); + } + run_next_test(); +}); + +// Bug 960014 - Make nsStandardURL::SetHost less magical around IPv6 +add_test(function test_ipv6() { + var url = stringToURL("http://example.com"); + url = url.mutate().setHost("[2001::1]").finalize(); + Assert.equal(url.host, "2001::1"); + + url = stringToURL("http://example.com"); + url = url.mutate().setHostPort("[2001::1]:30").finalize(); + Assert.equal(url.host, "2001::1"); + Assert.equal(url.port, 30); + Assert.equal(url.hostPort, "[2001::1]:30"); + + url = stringToURL("http://example.com"); + url = url.mutate().setHostPort("2001:1").finalize(); + Assert.equal(url.host, "0.0.7.209"); + Assert.equal(url.port, 1); + Assert.equal(url.hostPort, "0.0.7.209:1"); + run_next_test(); +}); + +add_test(function test_ipv6_fail() { + var url = stringToURL("http://example.com"); + + Assert.throws( + () => { + url = url.mutate().setHost("2001::1").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "missing brackets" + ); + Assert.throws( + () => { + url = url.mutate().setHost("[2001::1]:20").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "url.host with port" + ); + Assert.throws( + () => { + url = url.mutate().setHost("[2001::1").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "missing last bracket" + ); + Assert.throws( + () => { + url = url.mutate().setHost("2001::1]").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "missing first bracket" + ); + Assert.throws( + () => { + url = url.mutate().setHost("2001[::1]").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad bracket position" + ); + Assert.throws( + () => { + url = url.mutate().setHost("[]").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "empty IPv6 address" + ); + Assert.throws( + () => { + url = url.mutate().setHost("[hello]").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad IPv6 address" + ); + Assert.throws( + () => { + url = url.mutate().setHost("[192.168.1.1]").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad IPv6 address" + ); + Assert.throws( + () => { + url = url.mutate().setHostPort("2001::1").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "missing brackets" + ); + Assert.throws( + () => { + url = url.mutate().setHostPort("[2001::1]30").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "missing : after IP" + ); + Assert.throws( + () => { + url = url.mutate().setHostPort("[2001:1]").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad IPv6 address" + ); + Assert.throws( + () => { + url = url.mutate().setHostPort("[2001:1]10").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad IPv6 address" + ); + Assert.throws( + () => { + url = url.mutate().setHostPort("[2001:1]10:20").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad IPv6 address" + ); + Assert.throws( + () => { + url = url.mutate().setHostPort("[2001:1]:10:20").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad IPv6 address" + ); + Assert.throws( + () => { + url = url.mutate().setHostPort("[2001:1").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad IPv6 address" + ); + Assert.throws( + () => { + url = url.mutate().setHostPort("2001]:1").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad IPv6 address" + ); + Assert.throws( + () => { + url = url.mutate().setHostPort("2001:1]").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad IPv6 address" + ); + Assert.throws( + () => { + url = url.mutate().setHostPort("").finalize(); + }, + /NS_ERROR_UNEXPECTED/, + "Empty hostPort should fail" + ); + + // These checks used to fail, but now don't (see bug 1433958 comment 57) + url = url.mutate().setHostPort("[2001::1]:").finalize(); + Assert.equal(url.spec, "http://[2001::1]/"); + url = url.mutate().setHostPort("[2002::1]:bad").finalize(); + Assert.equal(url.spec, "http://[2002::1]/"); + + run_next_test(); +}); + +add_test(function test_clearedSpec() { + var url = stringToURL("http://example.com/path"); + Assert.throws( + () => { + url = url.mutate().setSpec("http: example").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "set bad spec" + ); + Assert.throws( + () => { + url = url.mutate().setSpec("").finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "set empty spec" + ); + Assert.equal(url.spec, "http://example.com/path"); + url = url + .mutate() + .setHost("allizom.org") + .finalize() + .QueryInterface(Ci.nsIURL); + + var ref = stringToURL("http://allizom.org/path"); + symmetricEquality(true, url, ref); + run_next_test(); +}); + +add_test(function test_escapeBrackets() { + // Query + var url = stringToURL("http://example.com/?a[x]=1"); + Assert.equal(url.spec, "http://example.com/?a[x]=1"); + + url = stringToURL("http://example.com/?a%5Bx%5D=1"); + Assert.equal(url.spec, "http://example.com/?a%5Bx%5D=1"); + + url = stringToURL("http://[2001::1]/?a[x]=1"); + Assert.equal(url.spec, "http://[2001::1]/?a[x]=1"); + + url = stringToURL("http://[2001::1]/?a%5Bx%5D=1"); + Assert.equal(url.spec, "http://[2001::1]/?a%5Bx%5D=1"); + + // Path + url = stringToURL("http://example.com/brackets[x]/test"); + Assert.equal(url.spec, "http://example.com/brackets[x]/test"); + + url = stringToURL("http://example.com/a%5Bx%5D/test"); + Assert.equal(url.spec, "http://example.com/a%5Bx%5D/test"); + run_next_test(); +}); + +add_test(function test_escapeQuote() { + var url = stringToURL("http://example.com/#'"); + Assert.equal(url.spec, "http://example.com/#'"); + Assert.equal(url.ref, "'"); + url = url.mutate().setRef("test'test").finalize(); + Assert.equal(url.spec, "http://example.com/#test'test"); + Assert.equal(url.ref, "test'test"); + run_next_test(); +}); + +add_test(function test_apostropheEncoding() { + // For now, single quote is escaped everywhere _except_ the path. + // This policy is controlled by the bitmask in nsEscape.cpp::EscapeChars[] + var url = stringToURL("http://example.com/dir'/file'.ext'"); + Assert.equal(url.spec, "http://example.com/dir'/file'.ext'"); + run_next_test(); +}); + +add_test(function test_accentEncoding() { + var url = stringToURL("http://example.com/?hello=`"); + Assert.equal(url.spec, "http://example.com/?hello=`"); + Assert.equal(url.query, "hello=`"); + + url = stringToURL("http://example.com/?hello=%2C"); + Assert.equal(url.spec, "http://example.com/?hello=%2C"); + Assert.equal(url.query, "hello=%2C"); + run_next_test(); +}); + +add_test( + { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" }, + function test_percentDecoding() { + var url = stringToURL("http://%70%61%73%74%65%62%69%6E.com"); + Assert.equal(url.spec, "http://pastebin.com/"); + + // Disallowed hostname characters are rejected even when percent encoded + Assert.throws( + () => { + url = stringToURL("http://example.com%0a%23.google.com/"); + }, + /NS_ERROR_MALFORMED_URI/, + "invalid characters are not allowed" + ); + run_next_test(); + } +); + +add_test(function test_hugeStringThrows() { + let prefs = Services.prefs; + let maxLen = prefs.getIntPref("network.standard-url.max-length"); + let url = stringToURL("http://test:test@example.com"); + + let hugeString = new Array(maxLen + 1).fill("a").join(""); + let setters = [ + { method: "setSpec", qi: Ci.nsIURIMutator }, + { method: "setUsername", qi: Ci.nsIURIMutator }, + { method: "setPassword", qi: Ci.nsIURIMutator }, + { method: "setFilePath", qi: Ci.nsIURIMutator }, + { method: "setHostPort", qi: Ci.nsIURIMutator }, + { method: "setHost", qi: Ci.nsIURIMutator }, + { method: "setUserPass", qi: Ci.nsIURIMutator }, + { method: "setPathQueryRef", qi: Ci.nsIURIMutator }, + { method: "setQuery", qi: Ci.nsIURIMutator }, + { method: "setRef", qi: Ci.nsIURIMutator }, + { method: "setScheme", qi: Ci.nsIURIMutator }, + { method: "setFileName", qi: Ci.nsIURLMutator }, + { method: "setFileExtension", qi: Ci.nsIURLMutator }, + { method: "setFileBaseName", qi: Ci.nsIURLMutator }, + ]; + + for (let prop of setters) { + Assert.throws( + () => + (url = url + .mutate() + .QueryInterface(prop.qi) + [prop.method](hugeString) + .finalize()), + /NS_ERROR_MALFORMED_URI/, + `Passing a huge string to "${prop.method}" should throw` + ); + } + + run_next_test(); +}); + +add_test(function test_filterWhitespace() { + let url = stringToURL( + " \r\n\th\nt\rt\tp://ex\r\n\tample.com/path\r\n\t/\r\n\tto the/fil\r\n\te.e\r\n\txt?que\r\n\try#ha\r\n\tsh \r\n\t " + ); + Assert.equal( + url.spec, + "http://example.com/path/to%20the/file.ext?query#hash" + ); + + // These setters should escape \r\n\t, not filter them. + url = stringToURL("http://test.com/path?query#hash"); + url = url.mutate().setFilePath("pa\r\n\tth").finalize(); + Assert.equal(url.spec, "http://test.com/pa%0D%0A%09th?query#hash"); + url = url.mutate().setQuery("que\r\n\try").finalize(); + Assert.equal(url.spec, "http://test.com/pa%0D%0A%09th?query#hash"); + url = url.mutate().setRef("ha\r\n\tsh").finalize(); + Assert.equal(url.spec, "http://test.com/pa%0D%0A%09th?query#hash"); + url = url + .mutate() + .QueryInterface(Ci.nsIURLMutator) + .setFileName("fi\r\n\tle.name") + .finalize(); + Assert.equal(url.spec, "http://test.com/fi%0D%0A%09le.name?query#hash"); + + run_next_test(); +}); + +add_test(function test_backslashReplacement() { + var url = stringToURL( + "http:\\\\test.com\\path/to\\file?query\\backslash#hash\\" + ); + Assert.equal( + url.spec, + "http://test.com/path/to/file?query\\backslash#hash\\" + ); + + url = stringToURL("http:\\\\test.com\\example.org/path\\to/file"); + Assert.equal(url.spec, "http://test.com/example.org/path/to/file"); + Assert.equal(url.host, "test.com"); + Assert.equal(url.pathQueryRef, "/example.org/path/to/file"); + + run_next_test(); +}); + +add_test(function test_authority_host() { + Assert.throws( + () => { + stringToURL("http:"); + }, + /NS_ERROR_MALFORMED_URI/, + "TYPE_AUTHORITY should have host" + ); + Assert.throws( + () => { + stringToURL("http:///"); + }, + /NS_ERROR_MALFORMED_URI/, + "TYPE_AUTHORITY should have host" + ); + + run_next_test(); +}); + +add_test(function test_trim_C0_and_space() { + var url = stringToURL( + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f http://example.com/ \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f " + ); + Assert.equal(url.spec, "http://example.com/"); + url = url + .mutate() + .setSpec( + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f http://test.com/ \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f " + ) + .finalize(); + Assert.equal(url.spec, "http://test.com/"); + Assert.throws( + () => { + url = url + .mutate() + .setSpec( + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 " + ) + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "set empty spec" + ); + run_next_test(); +}); + +// This tests that C0-and-space characters in the path, query and ref are +// percent encoded. +add_test(function test_encode_C0_and_space() { + function toHex(d) { + var hex = d.toString(16); + if (hex.length == 1) { + hex = "0" + hex; + } + return hex.toUpperCase(); + } + + for (var i = 0x0; i <= 0x20; i++) { + // These characters get filtered - they are not encoded. + if ( + String.fromCharCode(i) == "\r" || + String.fromCharCode(i) == "\n" || + String.fromCharCode(i) == "\t" + ) { + continue; + } + let url = stringToURL( + "http://example.com/pa" + + String.fromCharCode(i) + + "th?qu" + + String.fromCharCode(i) + + "ery#ha" + + String.fromCharCode(i) + + "sh" + ); + Assert.equal( + url.spec, + "http://example.com/pa%" + + toHex(i) + + "th?qu%" + + toHex(i) + + "ery#ha%" + + toHex(i) + + "sh" + ); + } + + // Additionally, we need to check the setters. + let url = stringToURL("http://example.com/path?query#hash"); + url = url.mutate().setFilePath("pa\0th").finalize(); + Assert.equal(url.spec, "http://example.com/pa%00th?query#hash"); + url = url.mutate().setQuery("qu\0ery").finalize(); + Assert.equal(url.spec, "http://example.com/pa%00th?qu%00ery#hash"); + url = url.mutate().setRef("ha\0sh").finalize(); + Assert.equal(url.spec, "http://example.com/pa%00th?qu%00ery#ha%00sh"); + url = url + .mutate() + .QueryInterface(Ci.nsIURLMutator) + .setFileName("fi\0le.name") + .finalize(); + Assert.equal(url.spec, "http://example.com/fi%00le.name?qu%00ery#ha%00sh"); + + run_next_test(); +}); + +add_test(function test_ipv4Normalize() { + var localIPv4s = [ + "http://127.0.0.1", + "http://127.0.1", + "http://127.1", + "http://2130706433", + "http://0177.00.00.01", + "http://0177.00.01", + "http://0177.01", + "http://00000000000000000000000000177.0000000.0000000.0001", + "http://000000177.0000001", + "http://017700000001", + "http://0x7f.0x00.0x00.0x01", + "http://0x7f.0x01", + "http://0x7f000001", + "http://0x007f.0x0000.0x0000.0x0001", + "http://000177.0.00000.0x0001", + "http://127.0.0.1.", + ].map(stringToURL); + + let url; + for (url of localIPv4s) { + Assert.equal(url.spec, "http://127.0.0.1/"); + } + + // These should treated as a domain instead of an IPv4. + var nonIPv4s = [ + "http://0xfffffffff/", + "http://0x100000000/", + "http://4294967296/", + "http://1.2.0x10000/", + "http://1.0x1000000/", + "http://256.0.0.1/", + "http://1.256.1/", + "http://-1.0.0.0/", + "http://1.2.3.4.5/", + "http://010000000000000000/", + "http://2+3/", + "http://0.0.0.-1/", + "http://1.2.3.4../", + "http://1..2/", + "http://.1.2.3.4/", + "resource://123/", + "resource://4294967296/", + ]; + var spec; + for (spec of nonIPv4s) { + url = stringToURL(spec); + Assert.equal(url.spec, spec); + } + + url = stringToURL("resource://path/to/resource/"); + url = url.mutate().setHost("123").finalize(); + Assert.equal(url.host, "123"); + + run_next_test(); +}); + +add_test(function test_invalidHostChars() { + var url = stringToURL("http://example.org/"); + for (let i = 0; i <= 0x20; i++) { + Assert.throws( + () => { + url = url + .mutate() + .setHost("a" + String.fromCharCode(i) + "b") + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "Trying to set hostname containing char code: " + i + ); + } + for (let c of '@[]*<>|:"') { + Assert.throws( + () => { + url = url + .mutate() + .setHost("a" + c) + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "Trying to set hostname containing char: " + c + ); + } + + // It also can't contain /, \, #, ?, but we treat these characters as + // hostname separators, so there is no way to set them and fail. + run_next_test(); +}); + +add_test(function test_normalize_ipv6() { + var url = stringToURL("http://example.com"); + url = url.mutate().setHost("[::192.9.5.5]").finalize(); + Assert.equal(url.spec, "http://[::c009:505]/"); + + run_next_test(); +}); + +add_test(function test_emptyPassword() { + var url = stringToURL("http://a:@example.com"); + Assert.equal(url.spec, "http://a@example.com/"); + url = url.mutate().setPassword("pp").finalize(); + Assert.equal(url.spec, "http://a:pp@example.com/"); + url = url.mutate().setPassword("").finalize(); + Assert.equal(url.spec, "http://a@example.com/"); + url = url.mutate().setUserPass("xxx:").finalize(); + Assert.equal(url.spec, "http://xxx@example.com/"); + url = url.mutate().setPassword("zzzz").finalize(); + Assert.equal(url.spec, "http://xxx:zzzz@example.com/"); + url = url.mutate().setUserPass("xxxxx:yyyyyy").finalize(); + Assert.equal(url.spec, "http://xxxxx:yyyyyy@example.com/"); + url = url.mutate().setUserPass("z:").finalize(); + Assert.equal(url.spec, "http://z@example.com/"); + url = url.mutate().setPassword("ppppppppppp").finalize(); + Assert.equal(url.spec, "http://z:ppppppppppp@example.com/"); + + url = stringToURL("http://example.com"); + url = url.mutate().setPassword("").finalize(); // Still empty. Should work. + Assert.equal(url.spec, "http://example.com/"); + + run_next_test(); +}); + +add_test(function test_emptyUser() { + let url = stringToURL("http://:a@example.com/path/to/something?query#hash"); + Assert.equal(url.spec, "http://:a@example.com/path/to/something?query#hash"); + url = stringToURL("http://:@example.com/path/to/something?query#hash"); + Assert.equal(url.spec, "http://example.com/path/to/something?query#hash"); + + const kurl = stringToURL( + "http://user:pass@example.com:8888/path/to/something?query#hash" + ); + url = kurl.mutate().setUsername("").finalize(); + Assert.equal( + url.spec, + "http://:pass@example.com:8888/path/to/something?query#hash" + ); + Assert.equal(url.host, "example.com"); + Assert.equal(url.hostPort, "example.com:8888"); + Assert.equal(url.filePath, "/path/to/something"); + Assert.equal(url.query, "query"); + Assert.equal(url.ref, "hash"); + url = kurl.mutate().setUserPass(":pass1").finalize(); + Assert.equal( + url.spec, + "http://:pass1@example.com:8888/path/to/something?query#hash" + ); + Assert.equal(url.host, "example.com"); + Assert.equal(url.hostPort, "example.com:8888"); + Assert.equal(url.filePath, "/path/to/something"); + Assert.equal(url.query, "query"); + Assert.equal(url.ref, "hash"); + url = url.mutate().setUsername("user2").finalize(); + Assert.equal( + url.spec, + "http://user2:pass1@example.com:8888/path/to/something?query#hash" + ); + Assert.equal(url.host, "example.com"); + url = url.mutate().setUserPass(":pass234").finalize(); + Assert.equal( + url.spec, + "http://:pass234@example.com:8888/path/to/something?query#hash" + ); + Assert.equal(url.host, "example.com"); + url = url.mutate().setUserPass("").finalize(); + Assert.equal( + url.spec, + "http://example.com:8888/path/to/something?query#hash" + ); + Assert.equal(url.host, "example.com"); + url = url.mutate().setPassword("pa").finalize(); + Assert.equal( + url.spec, + "http://:pa@example.com:8888/path/to/something?query#hash" + ); + Assert.equal(url.host, "example.com"); + url = url.mutate().setUserPass("user:pass").finalize(); + symmetricEquality(true, url.QueryInterface(Ci.nsIURL), kurl); + + url = stringToURL("http://example.com:8888/path/to/something?query#hash"); + url = url.mutate().setPassword("pass").finalize(); + Assert.equal( + url.spec, + "http://:pass@example.com:8888/path/to/something?query#hash" + ); + url = url.mutate().setUsername("").finalize(); + Assert.equal( + url.spec, + "http://:pass@example.com:8888/path/to/something?query#hash" + ); + + url = stringToURL("http://example.com:8888"); + url = url.mutate().setUsername("user").finalize(); + url = url.mutate().setUsername("").finalize(); + Assert.equal(url.spec, "http://example.com:8888/"); + + url = stringToURL("http://:pass@example.com"); + Assert.equal(url.spec, "http://:pass@example.com/"); + url = url.mutate().setPassword("").finalize(); + Assert.equal(url.spec, "http://example.com/"); + url = url.mutate().setUserPass("user:pass").finalize(); + Assert.equal(url.spec, "http://user:pass@example.com/"); + Assert.equal(url.host, "example.com"); + url = url.mutate().setUserPass("u:p").finalize(); + Assert.equal(url.spec, "http://u:p@example.com/"); + Assert.equal(url.host, "example.com"); + url = url.mutate().setUserPass("u1:p23").finalize(); + Assert.equal(url.spec, "http://u1:p23@example.com/"); + Assert.equal(url.host, "example.com"); + url = url.mutate().setUsername("u").finalize(); + Assert.equal(url.spec, "http://u:p23@example.com/"); + Assert.equal(url.host, "example.com"); + url = url.mutate().setPassword("p").finalize(); + Assert.equal(url.spec, "http://u:p@example.com/"); + Assert.equal(url.host, "example.com"); + + url = url.mutate().setUserPass("u2:p2").finalize(); + Assert.equal(url.spec, "http://u2:p2@example.com/"); + Assert.equal(url.host, "example.com"); + url = url.mutate().setUserPass("u23:p23").finalize(); + Assert.equal(url.spec, "http://u23:p23@example.com/"); + Assert.equal(url.host, "example.com"); + + run_next_test(); +}); + +registerCleanupFunction(function () { + gPrefs.clearUserPref("network.standard-url.punycode-host"); +}); + +add_test(function test_idna_host() { + // See bug 945240 - this test makes sure that URLs return a punycode hostname + let url = stringToURL( + "http://user:password@ält.example.org:8080/path?query#etc" + ); + equal(url.host, "xn--lt-uia.example.org"); + equal(url.hostPort, "xn--lt-uia.example.org:8080"); + equal(url.prePath, "http://user:password@xn--lt-uia.example.org:8080"); + equal( + url.spec, + "http://user:password@xn--lt-uia.example.org:8080/path?query#etc" + ); + equal( + url.specIgnoringRef, + "http://user:password@xn--lt-uia.example.org:8080/path?query" + ); + equal( + url + .QueryInterface(Ci.nsISensitiveInfoHiddenURI) + .getSensitiveInfoHiddenSpec(), + "http://user:****@xn--lt-uia.example.org:8080/path?query#etc" + ); + + equal(url.displayHost, "ält.example.org"); + equal(url.displayHostPort, "ält.example.org:8080"); + equal( + url.displaySpec, + "http://user:password@ält.example.org:8080/path?query#etc" + ); + + equal(url.asciiHost, "xn--lt-uia.example.org"); + equal(url.asciiHostPort, "xn--lt-uia.example.org:8080"); + equal( + url.asciiSpec, + "http://user:password@xn--lt-uia.example.org:8080/path?query#etc" + ); + + url = url.mutate().setRef("").finalize(); // SetRef calls InvalidateCache() + equal( + url.spec, + "http://user:password@xn--lt-uia.example.org:8080/path?query" + ); + equal( + url.displaySpec, + "http://user:password@ält.example.org:8080/path?query" + ); + equal( + url.asciiSpec, + "http://user:password@xn--lt-uia.example.org:8080/path?query" + ); + + url = stringToURL("http://user:password@www.ält.com:8080/path?query#etc"); + url = url.mutate().setRef("").finalize(); + equal(url.spec, "http://user:password@www.xn--lt-uia.com:8080/path?query"); + + run_next_test(); +}); + +add_test( + { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" }, + function test_bug1517025() { + Assert.throws( + () => { + stringToURL("https://b%9a/"); + }, + /NS_ERROR_MALFORMED_URI/, + "bad URI" + ); + + Assert.throws( + () => { + stringToURL("https://b%9ª/"); + }, + /NS_ERROR_MALFORMED_URI/, + "bad URI" + ); + + let base = stringToURL( + "https://bug1517025.bmoattachments.org/attachment.cgi?id=9033787" + ); + Assert.throws( + () => { + Services.io.newURI("/\\b%9ª", "windows-1252", base); + }, + /NS_ERROR_MALFORMED_URI/, + "bad URI" + ); + + run_next_test(); + } +); + +add_task(async function test_emptyHostWithURLType() { + let makeURL = (str, type) => { + return Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIStandardURLMutator) + .init(type, 80, str, "UTF-8", null) + .finalize() + .QueryInterface(Ci.nsIURL); + }; + + let url = makeURL("http://foo.com/bar/", Ci.nsIStandardURL.URLTYPE_AUTHORITY); + Assert.throws( + () => url.mutate().setHost("").finalize().spec, + /NS_ERROR_UNEXPECTED/, + "Empty host is not allowed for URLTYPE_AUTHORITY" + ); + + url = makeURL("http://foo.com/bar/", Ci.nsIStandardURL.URLTYPE_STANDARD); + Assert.throws( + () => url.mutate().setHost("").finalize().spec, + /NS_ERROR_UNEXPECTED/, + "Empty host is not allowed for URLTYPE_STANDARD" + ); + + url = makeURL("http://foo.com/bar/", Ci.nsIStandardURL.URLTYPE_NO_AUTHORITY); + equal( + url.spec, + "http:///bar/", + "Host is removed when parsing URLTYPE_NO_AUTHORITY" + ); + equal( + url.mutate().setHost("").finalize().spec, + "http:///bar/", + "Setting an empty host does nothing for URLTYPE_NO_AUTHORITY" + ); + Assert.throws( + () => url.mutate().setHost("something").finalize().spec, + /NS_ERROR_UNEXPECTED/, + "Setting a non-empty host is not allowed for URLTYPE_NO_AUTHORITY" + ); + equal( + url.mutate().setHost("#j").finalize().spec, + "http:///bar/", + "Setting a pseudo-empty host does nothing for URLTYPE_NO_AUTHORITY" + ); + + url = makeURL( + "http://example.org:123/foo?bar#baz", + Ci.nsIStandardURL.URLTYPE_AUTHORITY + ); + Assert.throws( + () => url.mutate().setHost("#j").finalize().spec, + /NS_ERROR_UNEXPECTED/, + "A pseudo-empty host is not allowed for URLTYPE_AUTHORITY" + ); +}); + +add_task(async function test_fuzz() { + let makeURL = str => { + return ( + Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIStandardURLMutator) + .QueryInterface(Ci.nsIURIMutator) + // .init(type, 80, str, "UTF-8", null) + .setSpec(str) + .finalize() + .QueryInterface(Ci.nsIURL) + ); + }; + + Assert.throws(() => { + let url = makeURL("/"); + url.mutate().setHost("(").finalize(); + }, /NS_ERROR_MALFORMED_URI/); +}); + +add_task(async function test_bug1648493() { + let url = stringToURL("https://example.com/"); + url = url.mutate().setScheme("file").finalize(); + url = url.mutate().setScheme("resource").finalize(); + url = url.mutate().setPassword("ê").finalize(); + url = url.mutate().setUsername("ç").finalize(); + url = url.mutate().setScheme("t").finalize(); + equal(url.spec, "t://%C3%83%C2%A7:%C3%83%C2%AA@example.com/"); + equal(url.username, "%C3%83%C2%A7"); +}); diff --git a/netwerk/test/unit/test_standardurl_default_port.js b/netwerk/test/unit/test_standardurl_default_port.js new file mode 100644 index 0000000000..a62edcf0e7 --- /dev/null +++ b/netwerk/test/unit/test_standardurl_default_port.js @@ -0,0 +1,58 @@ +/* -*- Mode: javascript; indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This test exercises the nsIStandardURL "setDefaultPort" API. */ + +"use strict"; + +function run_test() { + function stringToURL(str) { + return Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIStandardURLMutator) + .init(Ci.nsIStandardURL.URLTYPE_AUTHORITY, 80, str, "UTF-8", null) + .finalize() + .QueryInterface(Ci.nsIStandardURL); + } + + // Create a nsStandardURL: + var origUrlStr = "http://foo.com/"; + var stdUrl = stringToURL(origUrlStr); + Assert.equal(-1, stdUrl.port); + + // Changing default port shouldn't adjust the value returned by "port", + // or the string representation. + let def100Url = stdUrl + .mutate() + .QueryInterface(Ci.nsIStandardURLMutator) + .setDefaultPort(100) + .finalize(); + Assert.equal(-1, def100Url.port); + Assert.equal(def100Url.spec, origUrlStr); + + // Changing port directly should update .port and .spec, though: + let port200Url = stdUrl.mutate().setPort("200").finalize(); + Assert.equal(200, port200Url.port); + Assert.equal(port200Url.spec, "http://foo.com:200/"); + + // ...but then if we change default port to match the custom port, + // the custom port should reset to -1 and disappear from .spec: + let def200Url = port200Url + .mutate() + .QueryInterface(Ci.nsIStandardURLMutator) + .setDefaultPort(200) + .finalize(); + Assert.equal(-1, def200Url.port); + Assert.equal(def200Url.spec, origUrlStr); + + // And further changes to default port should not make custom port reappear. + let def300Url = def200Url + .mutate() + .QueryInterface(Ci.nsIStandardURLMutator) + .setDefaultPort(300) + .finalize(); + Assert.equal(-1, def300Url.port); + Assert.equal(def300Url.spec, origUrlStr); +} diff --git a/netwerk/test/unit/test_standardurl_port.js b/netwerk/test/unit/test_standardurl_port.js new file mode 100644 index 0000000000..76fdad6405 --- /dev/null +++ b/netwerk/test/unit/test_standardurl_port.js @@ -0,0 +1,53 @@ +"use strict"; + +function run_test() { + function makeURI(aURLSpec, aCharset) { + return Services.io.newURI(aURLSpec, aCharset); + } + + var httpURI = makeURI("http://foo.com"); + Assert.equal(-1, httpURI.port); + + // Setting to default shouldn't cause a change + httpURI = httpURI.mutate().setPort(80).finalize(); + Assert.equal(-1, httpURI.port); + + // Setting to default after setting to non-default shouldn't cause a change (bug 403480) + httpURI = httpURI.mutate().setPort(123).finalize(); + Assert.equal(123, httpURI.port); + httpURI = httpURI.mutate().setPort(80).finalize(); + Assert.equal(-1, httpURI.port); + Assert.ok(!/80/.test(httpURI.spec)); + + // URL parsers shouldn't set ports to default value (bug 407538) + httpURI = httpURI.mutate().setSpec("http://foo.com:81").finalize(); + Assert.equal(81, httpURI.port); + httpURI = httpURI.mutate().setSpec("http://foo.com:80").finalize(); + Assert.equal(-1, httpURI.port); + Assert.ok(!/80/.test(httpURI.spec)); + + httpURI = makeURI("http://foo.com"); + Assert.equal(-1, httpURI.port); + Assert.ok(!/80/.test(httpURI.spec)); + + httpURI = makeURI("http://foo.com:80"); + Assert.equal(-1, httpURI.port); + Assert.ok(!/80/.test(httpURI.spec)); + + httpURI = makeURI("http://foo.com:80"); + Assert.equal(-1, httpURI.port); + Assert.ok(!/80/.test(httpURI.spec)); + + httpURI = makeURI("https://foo.com"); + Assert.equal(-1, httpURI.port); + Assert.ok(!/443/.test(httpURI.spec)); + + httpURI = makeURI("https://foo.com:443"); + Assert.equal(-1, httpURI.port); + Assert.ok(!/443/.test(httpURI.spec)); + + // XXX URL parsers shouldn't set ports to default value, even when changing scheme? + // not really possible given current nsIURI impls + //httpURI.spec = "https://foo.com:443"; + //do_check_eq(-1, httpURI.port); +} diff --git a/netwerk/test/unit/test_streamcopier.js b/netwerk/test/unit/test_streamcopier.js new file mode 100644 index 0000000000..f550923546 --- /dev/null +++ b/netwerk/test/unit/test_streamcopier.js @@ -0,0 +1,63 @@ +"use strict"; + +var testStr = "This is a test. "; +for (var i = 0; i < 10; ++i) { + testStr += testStr; +} + +function run_test() { + // Set up our stream to copy + var inStr = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + inStr.setData(testStr, testStr.length); + + // Set up our destination stream. Make sure to use segments a good + // bit smaller than our data length. + Assert.ok(testStr.length > 1024 * 10); + var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(true, true, 1024, 0xffffffff, null); + + var streamCopier = Cc[ + "@mozilla.org/network/async-stream-copier;1" + ].createInstance(Ci.nsIAsyncStreamCopier); + streamCopier.init( + inStr, + pipe.outputStream, + null, + true, + true, + 1024, + true, + true + ); + + var ctx = {}; + ctx.wrappedJSObject = ctx; + + var observer = { + onStartRequest(aRequest) {}, + onStopRequest(aRequest, aStatusCode) { + Assert.equal(aStatusCode, 0); + var sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sis.init(pipe.inputStream); + var result = ""; + var temp; + try { + // Need this because read() can throw at EOF + while ((temp = sis.read(1024))) { + result += temp; + } + } catch (e) { + Assert.equal(e.result, Cr.NS_BASE_STREAM_CLOSED); + } + Assert.equal(result, testStr); + do_test_finished(); + }, + }; + + streamCopier.asyncCopy(observer, ctx); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_substituting_protocol_handler.js b/netwerk/test/unit/test_substituting_protocol_handler.js new file mode 100644 index 0000000000..00e5af0c21 --- /dev/null +++ b/netwerk/test/unit/test_substituting_protocol_handler.js @@ -0,0 +1,64 @@ +"use strict"; + +add_task(async function test_case_insensitive_substitutions() { + let resProto = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsISubstitutingProtocolHandler); + + let uri = Services.io.newFileURI(do_get_file("data")); + + resProto.setSubstitution("FooBar", uri); + resProto.setSubstitutionWithFlags("BarBaz", uri, 0); + + equal( + resProto.resolveURI(Services.io.newURI("resource://foobar/")), + uri.spec, + "Got correct resolved URI for setSubstitution" + ); + + equal( + resProto.resolveURI(Services.io.newURI("resource://foobar/")), + uri.spec, + "Got correct resolved URI for setSubstitutionWithFlags" + ); + + ok( + resProto.hasSubstitution("foobar"), + "hasSubstitution works with all-lower-case root" + ); + ok( + resProto.hasSubstitution("FooBar"), + "hasSubstitution works with mixed-case root" + ); + + equal( + resProto.getSubstitution("foobar").spec, + uri.spec, + "getSubstitution works with all-lower-case root" + ); + equal( + resProto.getSubstitution("FooBar").spec, + uri.spec, + "getSubstitution works with mixed-case root" + ); + + resProto.setSubstitution("foobar", null); + resProto.setSubstitution("barbaz", null); + + Assert.throws( + () => resProto.resolveURI(Services.io.newURI("resource://foobar/")), + e => e.result == Cr.NS_ERROR_NOT_AVAILABLE, + "Correctly unregistered case-insensitive substitution in setSubstitution" + ); + Assert.throws( + () => resProto.resolveURI(Services.io.newURI("resource://barbaz/")), + e => e.result == Cr.NS_ERROR_NOT_AVAILABLE, + "Correctly unregistered case-insensitive substitution in setSubstitutionWithFlags" + ); + + Assert.throws( + () => resProto.getSubstitution("foobar"), + e => e.result == Cr.NS_ERROR_NOT_AVAILABLE, + "foobar substitution has been removed" + ); +}); diff --git a/netwerk/test/unit/test_suspend_channel_before_connect.js b/netwerk/test/unit/test_suspend_channel_before_connect.js new file mode 100644 index 0000000000..038ec6e2d5 --- /dev/null +++ b/netwerk/test/unit/test_suspend_channel_before_connect.js @@ -0,0 +1,95 @@ +"use strict"; + +var CC = Components.Constructor; + +const ServerSocket = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init" +); + +var obs = Services.obs; + +// A server that waits for a connect. If a channel is suspended it should not +// try to connect to the server until it is is resumed or not try at all if it +// is cancelled as in this test. +function TestServer() { + this.listener = ServerSocket(-1, true, -1); + this.port = this.listener.port; + this.listener.asyncListen(this); +} + +TestServer.prototype = { + onSocketAccepted(socket, trans) { + Assert.ok(false, "Socket should not have tried to connect!"); + }, + + onStopListening(socket) {}, + + stop() { + try { + this.listener.close(); + } catch (ignore) {} + }, +}; + +var requestListenerObserver = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(subject, topic, data) { + if ( + topic === "http-on-modify-request" && + subject instanceof Ci.nsIHttpChannel + ) { + var chan = subject.QueryInterface(Ci.nsIHttpChannel); + chan.suspend(); + var obs = Cc["@mozilla.org/observer-service;1"].getService(); + obs = obs.QueryInterface(Ci.nsIObserverService); + obs.removeObserver(this, "http-on-modify-request"); + + // Timers are bad, but we need to wait to see that we are not trying to + // connect to the server. There are no other event since nothing should + // happen until we resume the channel. + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + () => { + chan.cancel(Cr.NS_BINDING_ABORTED); + chan.resume(); + }, + 1000, + Ci.nsITimer.TYPE_ONE_SHOT + ); + } + }, +}; + +var listener = { + onStartRequest: function test_onStartR(request) {}, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + executeSoon(run_next_test); + }, +}; + +// Add observer and start a channel. Observer is going to suspend the channel on +// "http-on-modify-request" even. If a channel is suspended so early it should +// not try to connect at all until it is resumed. In this case we are going to +// wait for some time and cancel the channel before resuming it. +add_test(function testNoConnectChannelCanceledEarly() { + let serv = new TestServer(); + + obs.addObserver(requestListenerObserver, "http-on-modify-request"); + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + serv.port, + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen(listener); + + registerCleanupFunction(function () { + serv.stop(); + }); +}); diff --git a/netwerk/test/unit/test_suspend_channel_on_authRetry.js b/netwerk/test/unit/test_suspend_channel_on_authRetry.js new file mode 100644 index 0000000000..3539b7a6fc --- /dev/null +++ b/netwerk/test/unit/test_suspend_channel_on_authRetry.js @@ -0,0 +1,262 @@ +// This file tests async handling of a channel suspended in DoAuthRetry +// notifying http-on-modify-request and http-on-before-connect observers. +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +var obs = Services.obs; + +var requestObserver = null; + +function AuthPrompt() {} + +AuthPrompt.prototype = { + user: "guest", + pass: "guest", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]), + + prompt: function ap1_prompt(title, text, realm, save, defaultText, result) { + do_throw("unexpected prompt call"); + }, + + promptUsernameAndPassword: function promptUP( + title, + text, + realm, + savePW, + user, + pw + ) { + user.value = this.user; + pw.value = this.pass; + + obs.addObserver(requestObserver, "http-on-before-connect"); + obs.addObserver(requestObserver, "http-on-modify-request"); + return true; + }, + + promptPassword: function promptPW(title, text, realm, save, pwd) { + do_throw("unexpected promptPassword call"); + }, +}; + +function requestListenerObserver( + suspendOnBeforeConnect, + suspendOnModifyRequest +) { + this.suspendOnModifyRequest = suspendOnModifyRequest; + this.suspendOnBeforeConnect = suspendOnBeforeConnect; +} + +requestListenerObserver.prototype = { + suspendOnModifyRequest: false, + suspendOnBeforeConnect: false, + gotOnBeforeConnect: false, + resumeOnBeforeConnect: false, + gotOnModifyRequest: false, + resumeOnModifyRequest: false, + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(subject, topic, data) { + if ( + topic === "http-on-before-connect" && + subject instanceof Ci.nsIHttpChannel + ) { + if (this.suspendOnBeforeConnect) { + let chan = subject.QueryInterface(Ci.nsIHttpChannel); + executeSoon(() => { + this.resumeOnBeforeConnect = true; + chan.resume(); + }); + this.gotOnBeforeConnect = true; + chan.suspend(); + } + } else if ( + topic === "http-on-modify-request" && + subject instanceof Ci.nsIHttpChannel + ) { + if (this.suspendOnModifyRequest) { + let chan = subject.QueryInterface(Ci.nsIHttpChannel); + executeSoon(() => { + this.resumeOnModifyRequest = true; + chan.resume(); + }); + this.gotOnModifyRequest = true; + chan.suspend(); + } + } + }, +}; + +function Requestor() {} + +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface: function requestor_gi(iid) { + if (iid.equals(Ci.nsIAuthPrompt)) { + // Allow the prompt to store state by caching it here + if (!this.prompt) { + this.prompt = new AuthPrompt(); + } + return this.prompt; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompt: null, +}; + +var listener = { + expectedCode: -1, // Uninitialized + + onStartRequest: function test_onStartR(request) { + try { + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code!"); + } + + if (!(request instanceof Ci.nsIHttpChannel)) { + do_throw("Expecting an HTTP channel"); + } + + Assert.equal(request.responseStatus, this.expectedCode); + // The request should be succeeded iff we expect 200 + Assert.equal(request.requestSucceeded, this.expectedCode == 200); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + Assert.equal(status, Cr.NS_ERROR_ABORT); + if (requestObserver.suspendOnBeforeConnect) { + Assert.ok( + requestObserver.gotOnBeforeConnect && + requestObserver.resumeOnBeforeConnect + ); + } + if (requestObserver.suspendOnModifyRequest) { + Assert.ok( + requestObserver.gotOnModifyRequest && + requestObserver.resumeOnModifyRequest + ); + } + obs.removeObserver(requestObserver, "http-on-before-connect"); + obs.removeObserver(requestObserver, "http-on-modify-request"); + moveToNextTest(); + }, +}; + +function makeChan(url, loadingUrl) { + var principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(loadingUrl), + {} + ); + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); +} + +var tests = [ + test_suspend_on_before_connect, + test_suspend_on_modify_request, + test_suspend_all, +]; + +var current_test = 0; + +var httpserv = null; + +function moveToNextTest() { + if (current_test < tests.length - 1) { + // First, gotta clear the auth cache + Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager) + .clearAll(); + + current_test++; + tests[current_test](); + } else { + do_test_pending(); + httpserv.stop(do_test_finished); + } + + do_test_finished(); +} + +function run_test() { + httpserv = new HttpServer(); + + httpserv.registerPathHandler("/auth", authHandler); + + httpserv.start(-1); + + tests[0](); +} + +function test_suspend_on_auth(suspendOnBeforeConnect, suspendOnModifyRequest) { + var chan = makeChan(URL + "/auth", URL); + requestObserver = new requestListenerObserver( + suspendOnBeforeConnect, + suspendOnModifyRequest + ); + chan.notificationCallbacks = new Requestor(); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_suspend_on_before_connect() { + test_suspend_on_auth(true, false); +} + +function test_suspend_on_modify_request() { + test_suspend_on_auth(false, true); +} + +function test_suspend_all() { + test_suspend_on_auth(true, true); +} + +// PATH HANDLERS + +// /auth +function authHandler(metadata, response) { + // btoa("guest:guest"), but that function is not available here + var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; + + var body; + if ( + metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader + ) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "success"; + } else { + // didn't know guest:guest, failure + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "failed"; + } + + response.bodyOutputStream.write(body, body.length); +} diff --git a/netwerk/test/unit/test_suspend_channel_on_examine.js b/netwerk/test/unit/test_suspend_channel_on_examine.js new file mode 100644 index 0000000000..8d05854790 --- /dev/null +++ b/netwerk/test/unit/test_suspend_channel_on_examine.js @@ -0,0 +1,76 @@ +// This file tests async handling of a channel suspended in http-on-modify-request. +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var obs = Services.obs; + +var baseUrl; + +function responseHandler(metadata, response) { + var text = "testing"; + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Set-Cookie", "chewy", false); + response.bodyOutputStream.write(text, text.length); +} + +function onExamineListener(callback) { + obs.addObserver( + { + observe(subject, topic, data) { + var obs = Cc["@mozilla.org/observer-service;1"].getService(); + obs = obs.QueryInterface(Ci.nsIObserverService); + obs.removeObserver(this, "http-on-examine-response"); + callback(subject.QueryInterface(Ci.nsIHttpChannel)); + }, + }, + "http-on-examine-response" + ); +} + +function startChannelRequest(baseUrl, flags, callback) { + var chan = NetUtil.newChannel({ + uri: baseUrl, + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen(new ChannelListener(callback, null, flags)); +} + +// We first make a request that we'll cancel asynchronously. The response will +// still contain the set-cookie header. Then verify the cookie was not actually +// retained. +add_test(function testAsyncCancel() { + onExamineListener(chan => { + // Suspend the channel then yield to make this async. + chan.suspend(); + Promise.resolve().then(() => { + chan.cancel(Cr.NS_BINDING_ABORTED); + chan.resume(); + }); + }); + startChannelRequest(baseUrl, CL_EXPECT_FAILURE, (request, data, context) => { + Assert.ok(!data, "no response"); + + Assert.equal( + Services.cookies.countCookiesFromHost("localhost"), + 0, + "no cookies set" + ); + + executeSoon(run_next_test); + }); +}); + +function run_test() { + var httpServer = new HttpServer(); + httpServer.registerPathHandler("/", responseHandler); + httpServer.start(-1); + + baseUrl = `http://localhost:${httpServer.identity.primaryPort}`; + + run_next_test(); + + registerCleanupFunction(function () { + httpServer.stop(() => {}); + }); +} diff --git a/netwerk/test/unit/test_suspend_channel_on_examine_merged_response.js b/netwerk/test/unit/test_suspend_channel_on_examine_merged_response.js new file mode 100644 index 0000000000..bd35d5cc9d --- /dev/null +++ b/netwerk/test/unit/test_suspend_channel_on_examine_merged_response.js @@ -0,0 +1,206 @@ +/* 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 file tests async handling of a channel suspended in +// notifying http-on-examine-merged-response observers. +// Note that this test is developed based on test_bug482601.js. +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserv = null; +var test_nr = 0; +var buffer = ""; +var observerCalled = false; +var channelResumed = false; + +var observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(subject, topic, data) { + if ( + topic === "http-on-examine-merged-response" && + subject instanceof Ci.nsIHttpChannel + ) { + var chan = subject.QueryInterface(Ci.nsIHttpChannel); + executeSoon(() => { + Assert.equal(channelResumed, false); + channelResumed = true; + chan.resume(); + }); + Assert.equal(observerCalled, false); + observerCalled = true; + chan.suspend(); + } + }, +}; + +var listener = { + onStartRequest(request) { + buffer = ""; + }, + + onDataAvailable(request, stream, offset, count) { + buffer = buffer.concat(read_stream(stream, count)); + }, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + Assert.equal(buffer, "0123456789"); + Assert.equal(channelResumed, true); + Assert.equal(observerCalled, true); + test_nr++; + do_timeout(0, do_test); + }, +}; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/path/partial", path_partial); + httpserv.registerPathHandler("/path/cached", path_cached); + httpserv.start(-1); + + Services.obs.addObserver(observer, "http-on-examine-merged-response"); + + do_timeout(0, do_test); + do_test_pending(); +} + +function do_test() { + if (test_nr < tests.length) { + tests[test_nr](); + } else { + Services.obs.removeObserver(observer, "http-on-examine-merged-response"); + httpserv.stop(do_test_finished); + } +} + +var tests = [test_partial, test_cached]; + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function storeCache(aCacheEntry, aResponseHeads, aContent) { + aCacheEntry.setMetaDataElement("request-method", "GET"); + aCacheEntry.setMetaDataElement("response-head", aResponseHeads); + aCacheEntry.setMetaDataElement("charset", "ISO-8859-1"); + + var oStream = aCacheEntry.openOutputStream(0, aContent.length); + var written = oStream.write(aContent, aContent.length); + if (written != aContent.length) { + do_throw( + "oStream.write has not written all data!\n" + + " Expected: " + + written + + "\n" + + " Actual: " + + aContent.length + + "\n" + ); + } + oStream.close(); + aCacheEntry.close(); +} + +function test_partial() { + observerCalled = false; + channelResumed = false; + asyncOpenCacheEntry( + "http://localhost:" + httpserv.identity.primaryPort + "/path/partial", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + test_partial2 + ); +} + +function test_partial2(status, entry) { + Assert.equal(status, Cr.NS_OK); + storeCache( + entry, + "HTTP/1.1 200 OK\r\n" + + "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Server: httpd.js\r\n" + + "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Accept-Ranges: bytes\r\n" + + "Content-Length: 10\r\n" + + "Content-Type: text/plain\r\n", + "0123" + ); + + observerCalled = false; + + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/path/partial" + ); + chan.asyncOpen(listener); +} + +function test_cached() { + observerCalled = false; + channelResumed = false; + asyncOpenCacheEntry( + "http://localhost:" + httpserv.identity.primaryPort + "/path/cached", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + test_cached2 + ); +} + +function test_cached2(status, entry) { + Assert.equal(status, Cr.NS_OK); + storeCache( + entry, + "HTTP/1.1 200 OK\r\n" + + "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Server: httpd.js\r\n" + + "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Accept-Ranges: bytes\r\n" + + "Content-Length: 10\r\n" + + "Content-Type: text/plain\r\n", + "0123456789" + ); + + observerCalled = false; + + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/path/cached" + ); + chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS; + chan.asyncOpen(listener); +} + +// PATHS + +// /path/partial +function path_partial(metadata, response) { + Assert.ok(metadata.hasHeader("If-Range")); + Assert.equal(metadata.getHeader("If-Range"), "Thu, 1 Jan 2009 00:00:00 GMT"); + Assert.ok(metadata.hasHeader("Range")); + Assert.equal(metadata.getHeader("Range"), "bytes=4-"); + + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader("Content-Range", "bytes 4-9/10", false); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Last-Modified", "Thu, 1 Jan 2009 00:00:00 GMT"); + + var body = "456789"; + response.bodyOutputStream.write(body, body.length); +} + +// /path/cached +function path_cached(metadata, response) { + Assert.ok(metadata.hasHeader("If-Modified-Since")); + Assert.equal( + metadata.getHeader("If-Modified-Since"), + "Thu, 1 Jan 2009 00:00:00 GMT" + ); + + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); +} diff --git a/netwerk/test/unit/test_suspend_channel_on_modified.js b/netwerk/test/unit/test_suspend_channel_on_modified.js new file mode 100644 index 0000000000..1a5090176c --- /dev/null +++ b/netwerk/test/unit/test_suspend_channel_on_modified.js @@ -0,0 +1,177 @@ +// This file tests async handling of a channel suspended in http-on-modify-request. +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var obs = Services.obs; + +var ios = Services.io; + +// baseUrl is always the initial connection attempt and is handled by +// failResponseHandler since every test expects that request will either be +// redirected or cancelled. +var baseUrl; + +function failResponseHandler(metadata, response) { + var text = "failure response"; + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(text, text.length); + Assert.ok(false, "Received request when we shouldn't."); +} + +function successResponseHandler(metadata, response) { + var text = "success response"; + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(text, text.length); + Assert.ok(true, "Received expected request."); +} + +function onModifyListener(callback) { + obs.addObserver( + { + observe(subject, topic, data) { + var obs = Cc["@mozilla.org/observer-service;1"].getService(); + obs = obs.QueryInterface(Ci.nsIObserverService); + obs.removeObserver(this, "http-on-modify-request"); + callback(subject.QueryInterface(Ci.nsIHttpChannel)); + }, + }, + "http-on-modify-request" + ); +} + +function startChannelRequest(baseUrl, flags, expectedResponse = null) { + var chan = NetUtil.newChannel({ + uri: baseUrl, + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen( + new ChannelListener( + (request, data, context) => { + if (expectedResponse) { + Assert.equal(data, expectedResponse); + } else { + Assert.ok(!data, "no response"); + } + executeSoon(run_next_test); + }, + null, + flags + ) + ); +} + +add_test(function testSimpleRedirect() { + onModifyListener(chan => { + chan.redirectTo(ios.newURI(`${baseUrl}/success`)); + }); + startChannelRequest(baseUrl, undefined, "success response"); +}); + +add_test(function testSimpleCancel() { + onModifyListener(chan => { + chan.cancel(Cr.NS_BINDING_ABORTED); + }); + startChannelRequest(baseUrl, CL_EXPECT_FAILURE); +}); + +add_test(function testSimpleCancelRedirect() { + onModifyListener(chan => { + chan.redirectTo(ios.newURI(`${baseUrl}/fail`)); + chan.cancel(Cr.NS_BINDING_ABORTED); + }); + startChannelRequest(baseUrl, CL_EXPECT_FAILURE); +}); + +// Test a request that will get redirected asynchronously. baseUrl should +// not be requested, we should receive the request for the redirectedUrl. +add_test(function testAsyncRedirect() { + onModifyListener(chan => { + // Suspend the channel then yield to make this async. + chan.suspend(); + Promise.resolve().then(() => { + chan.redirectTo(ios.newURI(`${baseUrl}/success`)); + chan.resume(); + }); + }); + startChannelRequest(baseUrl, undefined, "success response"); +}); + +add_test(function testSyncRedirect() { + onModifyListener(chan => { + chan.suspend(); + chan.redirectTo(ios.newURI(`${baseUrl}/success`)); + Promise.resolve().then(() => { + chan.resume(); + }); + }); + startChannelRequest(baseUrl, undefined, "success response"); +}); + +add_test(function testAsyncCancel() { + onModifyListener(chan => { + // Suspend the channel then yield to make this async. + chan.suspend(); + Promise.resolve().then(() => { + chan.cancel(Cr.NS_BINDING_ABORTED); + chan.resume(); + }); + }); + startChannelRequest(baseUrl, CL_EXPECT_FAILURE); +}); + +add_test(function testSyncCancel() { + onModifyListener(chan => { + chan.suspend(); + chan.cancel(Cr.NS_BINDING_ABORTED); + Promise.resolve().then(() => { + chan.resume(); + }); + }); + startChannelRequest(baseUrl, CL_EXPECT_FAILURE); +}); + +// Test request that will get redirected and cancelled asynchronously, +// ensure no connection is made. +add_test(function testAsyncCancelRedirect() { + onModifyListener(chan => { + // Suspend the channel then yield to make this async. + chan.suspend(); + Promise.resolve().then(() => { + chan.cancel(Cr.NS_BINDING_ABORTED); + chan.redirectTo(ios.newURI(`${baseUrl}/fail`)); + chan.resume(); + }); + }); + startChannelRequest(baseUrl, CL_EXPECT_FAILURE); +}); + +// Test a request that will get cancelled synchronously, ensure async redirect +// is not made. +add_test(function testSyncCancelRedirect() { + onModifyListener(chan => { + chan.suspend(); + chan.cancel(Cr.NS_BINDING_ABORTED); + Promise.resolve().then(() => { + chan.redirectTo(ios.newURI(`${baseUrl}/fail`)); + chan.resume(); + }); + }); + startChannelRequest(baseUrl, CL_EXPECT_FAILURE); +}); + +function run_test() { + var httpServer = new HttpServer(); + httpServer.registerPathHandler("/", failResponseHandler); + httpServer.registerPathHandler("/fail", failResponseHandler); + httpServer.registerPathHandler("/success", successResponseHandler); + httpServer.start(-1); + + baseUrl = `http://localhost:${httpServer.identity.primaryPort}`; + + run_next_test(); + + registerCleanupFunction(function () { + httpServer.stop(() => {}); + }); +} diff --git a/netwerk/test/unit/test_synthesized_response.js b/netwerk/test/unit/test_synthesized_response.js new file mode 100644 index 0000000000..9a3fce8832 --- /dev/null +++ b/netwerk/test/unit/test_synthesized_response.js @@ -0,0 +1,286 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function isParentProcess() { + let appInfo = Cc["@mozilla.org/xre/app-info;1"]; + return ( + !appInfo || + Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +if (isParentProcess()) { + // ensure the cache service is prepped when running the test + // We only do this in the main process, as the cache storage service leaks + // when instantiated in the content process. + Services.cache2; +} + +var gotOnProgress; +var gotOnStatus; + +function make_channel(url, body, cb) { + gotOnProgress = false; + gotOnStatus = false; + var chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.notificationCallbacks = { + numChecks: 0, + QueryInterface: ChromeUtils.generateQI([ + "nsINetworkInterceptController", + "nsIInterfaceRequestor", + "nsIProgressEventSink", + ]), + getInterface(iid) { + return this.QueryInterface(iid); + }, + onProgress(request, progress, progressMax) { + gotOnProgress = true; + }, + onStatus(request, status, statusArg) { + gotOnStatus = true; + }, + shouldPrepareForIntercept() { + Assert.equal(this.numChecks, 0); + this.numChecks++; + return true; + }, + channelIntercepted(channel) { + channel.QueryInterface(Ci.nsIInterceptedChannel); + if (body) { + var synthesized = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + synthesized.data = body; + + channel.startSynthesizedResponse(synthesized, null, null, "", false); + channel.finishSynthesizedResponse(); + } + if (cb) { + cb(channel); + } + return { + dispatch() {}, + }; + }, + }; + return chan; +} + +const REMOTE_BODY = "http handler body"; +const NON_REMOTE_BODY = "synthesized body"; +const NON_REMOTE_BODY_2 = "synthesized body #2"; + +function bodyHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.write(REMOTE_BODY); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/body", bodyHandler); + httpServer.start(-1); + + run_next_test(); +} + +function handle_synthesized_response(request, buffer) { + Assert.equal(buffer, NON_REMOTE_BODY); + Assert.ok(gotOnStatus); + Assert.ok(gotOnProgress); + run_next_test(); +} + +function handle_synthesized_response_2(request, buffer) { + Assert.equal(buffer, NON_REMOTE_BODY_2); + Assert.ok(gotOnStatus); + Assert.ok(gotOnProgress); + run_next_test(); +} + +function handle_remote_response(request, buffer) { + Assert.equal(buffer, REMOTE_BODY); + Assert.ok(gotOnStatus); + Assert.ok(gotOnProgress); + run_next_test(); +} + +// hit the network instead of synthesizing +add_test(function () { + var chan = make_channel(URL + "/body", null, function (chan) { + chan.resetInterception(false); + }); + chan.asyncOpen(new ChannelListener(handle_remote_response, null)); +}); + +// synthesize a response +add_test(function () { + var chan = make_channel(URL + "/body", NON_REMOTE_BODY); + chan.asyncOpen( + new ChannelListener(handle_synthesized_response, null, CL_ALLOW_UNKNOWN_CL) + ); +}); + +// hit the network instead of synthesizing, to test that no previous synthesized +// cache entry is used. +add_test(function () { + var chan = make_channel(URL + "/body", null, function (chan) { + chan.resetInterception(false); + }); + chan.asyncOpen(new ChannelListener(handle_remote_response, null)); +}); + +// synthesize a different response to ensure no previous response is cached +add_test(function () { + var chan = make_channel(URL + "/body", NON_REMOTE_BODY_2); + chan.asyncOpen( + new ChannelListener( + handle_synthesized_response_2, + null, + CL_ALLOW_UNKNOWN_CL + ) + ); +}); + +// ensure that the channel waits for a decision and synthesizes headers correctly +add_test(function () { + var chan = make_channel(URL + "/body", null, function (channel) { + do_timeout(100, function () { + var synthesized = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + synthesized.data = NON_REMOTE_BODY; + channel.synthesizeHeader("Content-Length", NON_REMOTE_BODY.length); + channel.startSynthesizedResponse(synthesized, null, null, "", false); + channel.finishSynthesizedResponse(); + }); + }); + chan.asyncOpen(new ChannelListener(handle_synthesized_response, null)); +}); + +// ensure that the channel waits for a decision +add_test(function () { + var chan = make_channel(URL + "/body", null, function (chan) { + do_timeout(100, function () { + chan.resetInterception(false); + }); + }); + chan.asyncOpen(new ChannelListener(handle_remote_response, null)); +}); + +// ensure that the intercepted channel supports suspend/resume +add_test(function () { + var chan = make_channel(URL + "/body", null, function (intercepted) { + var synthesized = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + synthesized.data = NON_REMOTE_BODY; + + // set the content-type to ensure that the stream converter doesn't hold up notifications + // and cause the test to fail + intercepted.synthesizeHeader("Content-Type", "text/plain"); + intercepted.startSynthesizedResponse(synthesized, null, null, "", false); + intercepted.finishSynthesizedResponse(); + }); + chan.asyncOpen( + new ChannelListener( + handle_synthesized_response, + null, + CL_ALLOW_UNKNOWN_CL | CL_SUSPEND | CL_EXPECT_3S_DELAY + ) + ); +}); + +// ensure that the intercepted channel can be cancelled +add_test(function () { + var chan = make_channel(URL + "/body", null, function (intercepted) { + intercepted.cancelInterception(Cr.NS_BINDING_ABORTED); + }); + chan.asyncOpen(new ChannelListener(run_next_test, null, CL_EXPECT_FAILURE)); +}); + +// ensure that the channel can't be cancelled via nsIInterceptedChannel after making a decision +add_test(function () { + var chan = make_channel(URL + "/body", null, function (chan) { + chan.resetInterception(false); + do_timeout(0, function () { + var gotexception = false; + try { + chan.cancelInterception(); + } catch (x) { + gotexception = true; + } + Assert.ok(gotexception); + }); + }); + chan.asyncOpen(new ChannelListener(handle_remote_response, null)); +}); + +// ensure that the intercepted channel can be canceled during the response +add_test(function () { + var chan = make_channel(URL + "/body", null, function (intercepted) { + var synthesized = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + synthesized.data = NON_REMOTE_BODY; + + let channel = intercepted.channel; + intercepted.startSynthesizedResponse(synthesized, null, null, "", false); + intercepted.finishSynthesizedResponse(); + channel.cancel(Cr.NS_BINDING_ABORTED); + }); + chan.asyncOpen( + new ChannelListener( + run_next_test, + null, + CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL + ) + ); +}); + +// ensure that the intercepted channel can be canceled before the response +add_test(function () { + var chan = make_channel(URL + "/body", null, function (intercepted) { + var synthesized = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + synthesized.data = NON_REMOTE_BODY; + + intercepted.channel.cancel(Cr.NS_BINDING_ABORTED); + + // This should not throw, but result in the channel firing callbacks + // with an error status. + intercepted.startSynthesizedResponse(synthesized, null, null, "", false); + intercepted.finishSynthesizedResponse(); + }); + chan.asyncOpen( + new ChannelListener( + run_next_test, + null, + CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL + ) + ); +}); + +// Ensure that nsIInterceptedChannel.channelIntercepted() can return an error. +// In this case we should automatically ResetInterception() and complete the +// network request. +add_test(function () { + var chan = make_channel(URL + "/body", null, function (chan) { + throw new Error("boom"); + }); + chan.asyncOpen(new ChannelListener(handle_remote_response, null)); +}); + +add_test(function () { + httpServer.stop(run_next_test); +}); diff --git a/netwerk/test/unit/test_throttlechannel.js b/netwerk/test/unit/test_throttlechannel.js new file mode 100644 index 0000000000..c48d752091 --- /dev/null +++ b/netwerk/test/unit/test_throttlechannel.js @@ -0,0 +1,46 @@ +// Test nsIThrottledInputChannel interface. +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +function test_handler(metadata, response) { + const originalBody = "the response"; + response.setHeader("Content-Type", "text/html", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(originalBody, originalBody.length); +} + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function run_test() { + let httpserver = new HttpServer(); + httpserver.start(-1); + const PORT = httpserver.identity.primaryPort; + + httpserver.registerPathHandler("/testdir", test_handler); + + let channel = make_channel("http://localhost:" + PORT + "/testdir"); + + let tq = Cc["@mozilla.org/network/throttlequeue;1"].createInstance( + Ci.nsIInputChannelThrottleQueue + ); + tq.init(1000, 1000); + + let tic = channel.QueryInterface(Ci.nsIThrottledInputChannel); + tic.throttleQueue = tq; + + channel.asyncOpen( + new ChannelListener(() => { + ok(tq.bytesProcessed() > 0, "throttled queue processed some bytes"); + + httpserver.stop(do_test_finished); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_throttlequeue.js b/netwerk/test/unit/test_throttlequeue.js new file mode 100644 index 0000000000..4fa1eaa0e2 --- /dev/null +++ b/netwerk/test/unit/test_throttlequeue.js @@ -0,0 +1,25 @@ +// Test ThrottleQueue initialization. +"use strict"; + +function init(tq, mean, max) { + let threw = false; + try { + tq.init(mean, max); + } catch (e) { + threw = true; + } + return !threw; +} + +function run_test() { + let tq = Cc["@mozilla.org/network/throttlequeue;1"].createInstance( + Ci.nsIInputChannelThrottleQueue + ); + + ok(!init(tq, 0, 50), "mean bytes cannot be 0"); + ok(!init(tq, 50, 0), "max bytes cannot be 0"); + ok(!init(tq, 0, 0), "mean and max bytes cannot be 0"); + ok(!init(tq, 70, 20), "max cannot be less than mean"); + + ok(init(tq, 2, 2), "valid initialization"); +} diff --git a/netwerk/test/unit/test_throttling.js b/netwerk/test/unit/test_throttling.js new file mode 100644 index 0000000000..6e34cb668c --- /dev/null +++ b/netwerk/test/unit/test_throttling.js @@ -0,0 +1,64 @@ +// Test nsIThrottledInputChannel interface. +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +function test_handler(metadata, response) { + const originalBody = "the response"; + response.setHeader("Content-Type", "text/html", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(originalBody, originalBody.length); +} + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function run_test() { + let httpserver = new HttpServer(); + httpserver.registerPathHandler("/testdir", test_handler); + httpserver.start(-1); + + const PORT = httpserver.identity.primaryPort; + const size = 4096; + + let sstream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + sstream.data = "x".repeat(size); + + let mime = Cc["@mozilla.org/network/mime-input-stream;1"].createInstance( + Ci.nsIMIMEInputStream + ); + mime.addHeader("Content-Type", "multipart/form-data; boundary=zzzzz"); + mime.setData(sstream); + + let tq = Cc["@mozilla.org/network/throttlequeue;1"].createInstance( + Ci.nsIInputChannelThrottleQueue + ); + // Make sure the request takes more than one read. + tq.init(100 + size / 2, 100 + size / 2); + + let channel = make_channel("http://localhost:" + PORT + "/testdir"); + channel + .QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(mime, "", mime.available()); + channel.requestMethod = "POST"; + + let tic = channel.QueryInterface(Ci.nsIThrottledInputChannel); + tic.throttleQueue = tq; + + let startTime = Date.now(); + channel.asyncOpen( + new ChannelListener(() => { + ok(Date.now() - startTime > 1000, "request took more than one second"); + + httpserver.stop(do_test_finished); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_tldservice_nextsubdomain.js b/netwerk/test/unit/test_tldservice_nextsubdomain.js new file mode 100644 index 0000000000..5a078aa809 --- /dev/null +++ b/netwerk/test/unit/test_tldservice_nextsubdomain.js @@ -0,0 +1,24 @@ +"use strict"; + +function run_test() { + var tests = [ + { data: "bar.foo.co.uk", result: "foo.co.uk" }, + { data: "foo.bar.foo.co.uk", result: "bar.foo.co.uk" }, + { data: "foo.co.uk", throw: true }, + { data: "co.uk", throw: true }, + { data: ".co.uk", throw: true }, + { data: "com", throw: true }, + { data: "tûlîp.foo.fr", result: "foo.fr" }, + { data: "tûlîp.fôû.fr", result: "xn--f-xgav.fr" }, + { data: "file://foo/bar", throw: true }, + ]; + + tests.forEach(function (test) { + try { + var r = Services.eTLD.getNextSubDomain(test.data); + Assert.equal(r, test.result); + } catch (e) { + Assert.ok(test.throw); + } + }); +} diff --git a/netwerk/test/unit/test_tls13_disabled.js b/netwerk/test/unit/test_tls13_disabled.js new file mode 100644 index 0000000000..3bcb6333aa --- /dev/null +++ b/netwerk/test/unit/test_tls13_disabled.js @@ -0,0 +1,93 @@ +/* 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/. */ + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("security.tls.version.max"); + http3_clear_prefs(); +}); + +let httpsUri; + +add_task(async function setup() { + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + httpsUri = "https://foo.example.com:" + h2Port + "/"; + + await http3_setup_tests("h3"); +}); + +let Listener = function () {}; + +Listener.prototype = { + resumed: false, + + onStartRequest: function testOnStartRequest(request) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + if (this.expect_http3) { + Assert.equal(httpVersion, "h3"); + } else { + Assert.notEqual(httpVersion, "h3"); + } + + this.finish(); + }, +}; + +function chanPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +async function test_http3_used(expect_http3) { + let listener = new Listener(); + listener.expect_http3 = expect_http3; + let chan = makeChan(httpsUri); + await chanPromise(chan, listener); +} + +add_task(async function test_tls13_pref() { + await test_http3_used(true); + // Try one more time. + await test_http3_used(true); + + // Disable TLS1.3 + Services.prefs.setIntPref("security.tls.version.max", 3); + await test_http3_used(false); + // Try one more time. + await test_http3_used(false); + + // Enable TLS1.3 + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + Services.prefs.setIntPref("security.tls.version.max", 4); + await test_http3_used(true); + // Try one more time. + await test_http3_used(true); +}); diff --git a/netwerk/test/unit/test_tls_flags.js b/netwerk/test/unit/test_tls_flags.js new file mode 100644 index 0000000000..876cd0ccea --- /dev/null +++ b/netwerk/test/unit/test_tls_flags.js @@ -0,0 +1,248 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +"use strict"; + +// a fork of test_be_conservative + +// Tests that nsIHttpChannelInternal.tlsFlags can be used to set the +// client max version level. Flags can also be used to set the +// level of intolerance rollback and to test out an experimental 1.3 +// hello, though they are not tested here. + +// Get a profile directory and ensure PSM initializes NSS. +do_get_profile(); +Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + +class InputStreamCallback { + constructor(output) { + this.output = output; + this.stopped = false; + } + + onInputStreamReady(stream) { + info("input stream ready"); + if (this.stopped) { + info("input stream callback stopped - bailing"); + return; + } + let available = 0; + try { + available = stream.available(); + } catch (e) { + // onInputStreamReady may fire when the stream has been closed. + equal( + e.result, + Cr.NS_BASE_STREAM_CLOSED, + "error should be NS_BASE_STREAM_CLOSED" + ); + } + if (available > 0) { + let request = NetUtil.readInputStreamToString(stream, available, { + charset: "utf8", + }); + ok( + request.startsWith("GET / HTTP/1.1\r\n"), + "Should get a simple GET / HTTP/1.1 request" + ); + let response = + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 2\r\n" + + "Content-Type: text/plain\r\n" + + "\r\nOK"; + let written = this.output.write(response, response.length); + equal( + written, + response.length, + "should have been able to write entire response" + ); + } + this.output.close(); + info("done with input stream ready"); + } + + stop() { + this.stopped = true; + this.output.close(); + } +} + +class TLSServerSecurityObserver { + constructor(input, output, expectedVersion) { + this.input = input; + this.output = output; + this.expectedVersion = expectedVersion; + this.callbacks = []; + this.stopped = false; + } + + onHandshakeDone(socket, status) { + info("TLS handshake done"); + info(`TLS version used: ${status.tlsVersionUsed}`); + info(this.expectedVersion); + equal( + status.tlsVersionUsed, + this.expectedVersion, + "expected version check" + ); + if (this.stopped) { + info("handshake done callback stopped - bailing"); + return; + } + + let callback = new InputStreamCallback(this.output); + this.callbacks.push(callback); + this.input.asyncWait(callback, 0, 0, Services.tm.currentThread); + } + + stop() { + this.stopped = true; + this.input.close(); + this.output.close(); + this.callbacks.forEach(callback => { + callback.stop(); + }); + } +} + +function startServer( + cert, + minServerVersion, + maxServerVersion, + expectedVersion +) { + let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance( + Ci.nsITLSServerSocket + ); + tlsServer.init(-1, true, -1); + tlsServer.serverCert = cert; + tlsServer.setVersionRange(minServerVersion, maxServerVersion); + tlsServer.setSessionTickets(false); + + let listener = { + securityObservers: [], + + onSocketAccepted(socket, transport) { + info("accepted TLS client connection"); + let connectionInfo = transport.securityCallbacks.getInterface( + Ci.nsITLSServerConnectionInfo + ); + let input = transport.openInputStream(0, 0, 0); + let output = transport.openOutputStream(0, 0, 0); + let securityObserver = new TLSServerSecurityObserver( + input, + output, + expectedVersion + ); + this.securityObservers.push(securityObserver); + connectionInfo.setSecurityObserver(securityObserver); + }, + + // For some reason we get input stream callback events after we've stopped + // listening, so this ensures we just drop those events. + onStopListening() { + info("onStopListening"); + this.securityObservers.forEach(observer => { + observer.stop(); + }); + }, + }; + tlsServer.asyncListen(listener); + return tlsServer; +} + +const hostname = "example.com"; + +function storeCertOverride(port, cert) { + let certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" + ].getService(Ci.nsICertOverrideService); + certOverrideService.rememberValidityOverride(hostname, port, {}, cert, true); +} + +function startClient(port, tlsFlags, expectSuccess) { + let req = new XMLHttpRequest(); + req.open("GET", `https://${hostname}:${port}`); + let internalChannel = req.channel.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.tlsFlags = tlsFlags; + return new Promise((resolve, reject) => { + req.onload = () => { + ok( + expectSuccess, + `should ${expectSuccess ? "" : "not "}have gotten load event` + ); + equal(req.responseText, "OK", "response text should be 'OK'"); + resolve(); + }; + req.onerror = () => { + ok( + !expectSuccess, + `should ${!expectSuccess ? "" : "not "}have gotten an error` + ); + resolve(); + }; + + req.send(); + }); +} + +add_task(async function () { + Services.prefs.setIntPref("security.tls.version.max", 4); + Services.prefs.setCharPref("network.dns.localDomains", hostname); + let cert = getTestServerCertificate(); + + // server that accepts 1.1->1.3 and a client max 1.3. expect 1.3 + info("TEST 1"); + let server = startServer( + cert, + Ci.nsITLSClientStatus.TLS_VERSION_1_1, + Ci.nsITLSClientStatus.TLS_VERSION_1_3, + Ci.nsITLSClientStatus.TLS_VERSION_1_3 + ); + storeCertOverride(server.port, cert); + await startClient(server.port, 4, true /*should succeed*/); + server.close(); + + // server that accepts 1.1->1.3 and a client max 1.1. expect 1.1 + info("TEST 2"); + server = startServer( + cert, + Ci.nsITLSClientStatus.TLS_VERSION_1_1, + Ci.nsITLSClientStatus.TLS_VERSION_1_3, + Ci.nsITLSClientStatus.TLS_VERSION_1_1 + ); + storeCertOverride(server.port, cert); + await startClient(server.port, 2, true); + server.close(); + + // server that accepts 1.2->1.2 and a client max 1.3. expect 1.2 + info("TEST 3"); + server = startServer( + cert, + Ci.nsITLSClientStatus.TLS_VERSION_1_2, + Ci.nsITLSClientStatus.TLS_VERSION_1_2, + Ci.nsITLSClientStatus.TLS_VERSION_1_2 + ); + storeCertOverride(server.port, cert); + await startClient(server.port, 4, true); + server.close(); + + // server that accepts 1.2->1.2 and a client max 1.1. expect fail + info("TEST 4"); + server = startServer( + cert, + Ci.nsITLSClientStatus.TLS_VERSION_1_2, + Ci.nsITLSClientStatus.TLS_VERSION_1_2, + 0 + ); + storeCertOverride(server.port, cert); + await startClient(server.port, 2, false); + + server.close(); +}); + +registerCleanupFunction(function () { + Services.prefs.clearUserPref("security.tls.version.max"); + Services.prefs.clearUserPref("network.dns.localDomains"); +}); diff --git a/netwerk/test/unit/test_tls_flags_separate_connections.js b/netwerk/test/unit/test_tls_flags_separate_connections.js new file mode 100644 index 0000000000..b0065da9e7 --- /dev/null +++ b/netwerk/test/unit/test_tls_flags_separate_connections.js @@ -0,0 +1,115 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +// This unit test ensures connections with different tlsFlags have their own +// connection pool. We verify this behavior by opening channels with different +// tlsFlags, and their connection info's hash keys should be different. + +// In the first round of this test, we record the hash key for each connection. +// In the second round, we check if each connection's hash key is consistent +// and different from other connection's hash key. + +let httpserv = null; +let gSecondRoundStarted = false; + +let randomFlagValues = [ + 0x00000000, + + 0xffffffff, + + 0x12345678, 0x12345678, + + 0x11111111, 0x22222222, + + 0xaaaaaaaa, 0x77777777, + + 0xbbbbbbbb, 0xcccccccc, +]; + +function handler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + let body = "0123456789"; + response.bodyOutputStream.write(body, body.length); +} + +function makeChan(url, tlsFlags) { + let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); + chan.QueryInterface(Ci.nsIHttpChannelInternal); + chan.tlsFlags = tlsFlags; + + return chan; +} + +let previousHashKeys = {}; + +function Listener(tlsFlags) { + this.tlsFlags = tlsFlags; +} + +let gTestsRun = 0; +Listener.prototype = { + onStartRequest(request) { + request + .QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + + Assert.equal(request.tlsFlags, this.tlsFlags); + + let hashKey = request.connectionInfoHashKey; + if (gSecondRoundStarted) { + // Compare the hash keys with the previous set ones. + // Hash keys should match if and only if their tlsFlags are the same. + for (let tlsFlags of randomFlagValues) { + if (tlsFlags == this.tlsFlags) { + Assert.equal(hashKey, previousHashKeys[tlsFlags]); + } else { + Assert.notEqual(hashKey, previousHashKeys[tlsFlags]); + } + } + } else { + // Set the hash keys in the first round. + previousHashKeys[this.tlsFlags] = hashKey; + } + }, + onDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + onStopRequest() { + gTestsRun++; + if (gTestsRun == randomFlagValues.length) { + gTestsRun = 0; + if (gSecondRoundStarted) { + // The second round finishes. + httpserv.stop(do_test_finished); + } else { + // The first round finishes. Do the second round. + gSecondRoundStarted = true; + doTest(); + } + } + }, +}; + +function doTest() { + for (let tlsFlags of randomFlagValues) { + let chan = makeChan(URL, tlsFlags); + let listener = new Listener(tlsFlags); + chan.asyncOpen(listener); + } +} + +function run_test() { + do_test_pending(); + httpserv = new HttpServer(); + httpserv.registerPathHandler("/", handler); + httpserv.start(-1); + + doTest(); +} diff --git a/netwerk/test/unit/test_tls_server.js b/netwerk/test/unit/test_tls_server.js new file mode 100644 index 0000000000..43ec2e5365 --- /dev/null +++ b/netwerk/test/unit/test_tls_server.js @@ -0,0 +1,343 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Need profile dir to store the key / cert +do_get_profile(); +// Ensure PSM is initialized +Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); +const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +const socketTransportService = Cc[ + "@mozilla.org/network/socket-transport-service;1" +].getService(Ci.nsISocketTransportService); + +const prefs = Services.prefs; + +function areCertsEqual(certA, certB) { + let derA = certA.getRawDER(); + let derB = certB.getRawDER(); + if (derA.length != derB.length) { + return false; + } + for (let i = 0; i < derA.length; i++) { + if (derA[i] != derB[i]) { + return false; + } + } + return true; +} + +function startServer( + cert, + expectingPeerCert, + clientCertificateConfig, + expectedVersion, + expectedVersionStr +) { + let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance( + Ci.nsITLSServerSocket + ); + tlsServer.init(-1, true, -1); + tlsServer.serverCert = cert; + + let input, output; + + let listener = { + onSocketAccepted(socket, transport) { + info("Accept TLS client connection"); + let connectionInfo = transport.securityCallbacks.getInterface( + Ci.nsITLSServerConnectionInfo + ); + connectionInfo.setSecurityObserver(listener); + input = transport.openInputStream(0, 0, 0); + output = transport.openOutputStream(0, 0, 0); + }, + onHandshakeDone(socket, status) { + info("TLS handshake done"); + if (expectingPeerCert) { + ok(!!status.peerCert, "Has peer cert"); + ok( + areCertsEqual(status.peerCert, cert), + "Peer cert matches expected cert" + ); + } else { + ok(!status.peerCert, "No peer cert (as expected)"); + } + + equal( + status.tlsVersionUsed, + expectedVersion, + "Using " + expectedVersionStr + ); + let expectedCipher; + if (expectedVersion >= 772) { + expectedCipher = "TLS_AES_128_GCM_SHA256"; + } else { + expectedCipher = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; + } + equal(status.cipherName, expectedCipher, "Using expected cipher"); + equal(status.keyLength, 128, "Using 128-bit key"); + equal(status.macLength, 128, "Using 128-bit MAC"); + + input.asyncWait( + { + onInputStreamReady(input) { + NetUtil.asyncCopy(input, output); + }, + }, + 0, + 0, + Services.tm.currentThread + ); + }, + onStopListening() { + info("onStopListening"); + input.close(); + output.close(); + }, + }; + + tlsServer.setSessionTickets(false); + tlsServer.setRequestClientCertificate(clientCertificateConfig); + + tlsServer.asyncListen(listener); + + return tlsServer; +} + +function storeCertOverride(port, cert) { + certOverrideService.rememberValidityOverride( + "127.0.0.1", + port, + {}, + cert, + true + ); +} + +function startClient(port, sendClientCert, expectingAlert, tlsVersion) { + gClientAuthDialogs.selectCertificate = sendClientCert; + let SSL_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE; + let SSL_ERROR_BAD_CERT_ALERT = SSL_ERROR_BASE + 17; + let SSL_ERROR_RX_CERTIFICATE_REQUIRED_ALERT = SSL_ERROR_BASE + 181; + let transport = socketTransportService.createTransport( + ["ssl"], + "127.0.0.1", + port, + null, + null + ); + let input; + let output; + + let inputDeferred = PromiseUtils.defer(); + let outputDeferred = PromiseUtils.defer(); + + let handler = { + onTransportStatus(transport, status) { + if (status === Ci.nsISocketTransport.STATUS_CONNECTED_TO) { + output.asyncWait(handler, 0, 0, Services.tm.currentThread); + } + }, + + onInputStreamReady(input) { + try { + let data = NetUtil.readInputStreamToString(input, input.available()); + equal(data, "HELLO", "Echoed data received"); + input.close(); + output.close(); + ok(!expectingAlert, "No cert alert expected"); + inputDeferred.resolve(); + } catch (e) { + let errorCode = -1 * (e.result & 0xffff); + if (expectingAlert) { + if ( + tlsVersion == Ci.nsITLSClientStatus.TLS_VERSION_1_2 && + errorCode == SSL_ERROR_BAD_CERT_ALERT + ) { + info("Got bad cert alert as expected for tls 1.2"); + input.close(); + output.close(); + inputDeferred.resolve(); + return; + } + if ( + tlsVersion == Ci.nsITLSClientStatus.TLS_VERSION_1_3 && + errorCode == SSL_ERROR_RX_CERTIFICATE_REQUIRED_ALERT + ) { + info("Got cert required alert as expected for tls 1.3"); + input.close(); + output.close(); + inputDeferred.resolve(); + return; + } + } + inputDeferred.reject(e); + } + }, + + onOutputStreamReady(output) { + try { + output.write("HELLO", 5); + info("Output to server written"); + outputDeferred.resolve(); + input = transport.openInputStream(0, 0, 0); + input.asyncWait(handler, 0, 0, Services.tm.currentThread); + } catch (e) { + let errorCode = -1 * (e.result & 0xffff); + if (errorCode == SSL_ERROR_BAD_CERT_ALERT) { + info("Server doesn't like client cert"); + } + outputDeferred.reject(e); + } + }, + }; + + transport.setEventSink(handler, Services.tm.currentThread); + output = transport.openOutputStream(0, 0, 0); + + return Promise.all([inputDeferred.promise, outputDeferred.promise]); +} + +// Replace the UI dialog that prompts the user to pick a client certificate. +const gClientAuthDialogs = { + _selectCertificate: false, + + set selectCertificate(value) { + this._selectCertificate = value; + }, + + chooseCertificate( + hostname, + port, + organization, + issuerOrg, + certList, + selectedIndex, + rememberClientAuthCertificate + ) { + rememberClientAuthCertificate.value = false; + if (this._selectCertificate) { + selectedIndex.value = 0; + return true; + } + return false; + }, + + QueryInterface: ChromeUtils.generateQI([Ci.nsIClientAuthDialogs]), +}; + +const ClientAuthDialogsContractID = "@mozilla.org/nsClientAuthDialogs;1"; +// On all platforms but Android, this component already exists, so this replaces +// it. On Android, the component does not exist, so this registers it. +if (AppConstants.platform != "android") { + MockRegistrar.register(ClientAuthDialogsContractID, gClientAuthDialogs); +} else { + const factory = { + createInstance(iid) { + return gClientAuthDialogs.QueryInterface(iid); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIFactory"]), + }; + const Cm = Components.manager; + const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + const cid = Services.uuid.generateUUID(); + registrar.registerFactory( + cid, + "A Mock for " + ClientAuthDialogsContractID, + ClientAuthDialogsContractID, + factory + ); +} + +const tests = [ + { + expectingPeerCert: true, + clientCertificateConfig: Ci.nsITLSServerSocket.REQUIRE_ALWAYS, + sendClientCert: true, + expectingAlert: false, + }, + { + expectingPeerCert: true, + clientCertificateConfig: Ci.nsITLSServerSocket.REQUIRE_ALWAYS, + sendClientCert: false, + expectingAlert: true, + }, + { + expectingPeerCert: true, + clientCertificateConfig: Ci.nsITLSServerSocket.REQUEST_ALWAYS, + sendClientCert: true, + expectingAlert: false, + }, + { + expectingPeerCert: false, + clientCertificateConfig: Ci.nsITLSServerSocket.REQUEST_ALWAYS, + sendClientCert: false, + expectingAlert: false, + }, + { + expectingPeerCert: false, + clientCertificateConfig: Ci.nsITLSServerSocket.REQUEST_NEVER, + sendClientCert: true, + expectingAlert: false, + }, + { + expectingPeerCert: false, + clientCertificateConfig: Ci.nsITLSServerSocket.REQUEST_NEVER, + sendClientCert: false, + expectingAlert: false, + }, +]; + +const versions = [ + { + prefValue: 3, + version: Ci.nsITLSClientStatus.TLS_VERSION_1_2, + versionStr: "TLS 1.2", + }, + { + prefValue: 4, + version: Ci.nsITLSClientStatus.TLS_VERSION_1_3, + versionStr: "TLS 1.3", + }, +]; + +add_task(async function () { + let cert = getTestServerCertificate(); + ok(!!cert, "Got self-signed cert"); + for (let v of versions) { + prefs.setIntPref("security.tls.version.max", v.prefValue); + for (let t of tests) { + let server = startServer( + cert, + t.expectingPeerCert, + t.clientCertificateConfig, + v.version, + v.versionStr + ); + storeCertOverride(server.port, cert); + await startClient( + server.port, + t.sendClientCert, + t.expectingAlert, + v.version + ); + server.close(); + } + } +}); + +registerCleanupFunction(function () { + prefs.clearUserPref("security.tls.version.max"); +}); diff --git a/netwerk/test/unit/test_tls_server_multiple_clients.js b/netwerk/test/unit/test_tls_server_multiple_clients.js new file mode 100644 index 0000000000..3058ec10f0 --- /dev/null +++ b/netwerk/test/unit/test_tls_server_multiple_clients.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Need profile dir to store the key / cert +do_get_profile(); +// Ensure PSM is initialized +Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + +const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +const socketTransportService = Cc[ + "@mozilla.org/network/socket-transport-service;1" +].getService(Ci.nsISocketTransportService); + +function startServer(cert) { + let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance( + Ci.nsITLSServerSocket + ); + tlsServer.init(-1, true, -1); + tlsServer.serverCert = cert; + + let input, output; + + let listener = { + onSocketAccepted(socket, transport) { + info("Accept TLS client connection"); + let connectionInfo = transport.securityCallbacks.getInterface( + Ci.nsITLSServerConnectionInfo + ); + connectionInfo.setSecurityObserver(listener); + input = transport.openInputStream(0, 0, 0); + output = transport.openOutputStream(0, 0, 0); + }, + onHandshakeDone(socket, status) { + info("TLS handshake done"); + + input.asyncWait( + { + onInputStreamReady(input) { + NetUtil.asyncCopy(input, output); + }, + }, + 0, + 0, + Services.tm.currentThread + ); + }, + onStopListening() {}, + }; + + tlsServer.setSessionTickets(false); + + tlsServer.asyncListen(listener); + + return tlsServer.port; +} + +function storeCertOverride(port, cert) { + certOverrideService.rememberValidityOverride( + "127.0.0.1", + port, + {}, + cert, + true + ); +} + +function startClient(port) { + let transport = socketTransportService.createTransport( + ["ssl"], + "127.0.0.1", + port, + null, + null + ); + let input; + let output; + + let inputDeferred = PromiseUtils.defer(); + let outputDeferred = PromiseUtils.defer(); + + let handler = { + onTransportStatus(transport, status) { + if (status === Ci.nsISocketTransport.STATUS_CONNECTED_TO) { + output.asyncWait(handler, 0, 0, Services.tm.currentThread); + } + }, + + onInputStreamReady(input) { + try { + let data = NetUtil.readInputStreamToString(input, input.available()); + equal(data, "HELLO", "Echoed data received"); + input.close(); + output.close(); + inputDeferred.resolve(); + } catch (e) { + inputDeferred.reject(e); + } + }, + + onOutputStreamReady(output) { + try { + output.write("HELLO", 5); + info("Output to server written"); + outputDeferred.resolve(); + input = transport.openInputStream(0, 0, 0); + input.asyncWait(handler, 0, 0, Services.tm.currentThread); + } catch (e) { + outputDeferred.reject(e); + } + }, + }; + + transport.setEventSink(handler, Services.tm.currentThread); + output = transport.openOutputStream(0, 0, 0); + + return Promise.all([inputDeferred.promise, outputDeferred.promise]); +} + +add_task(async function () { + let cert = getTestServerCertificate(); + ok(!!cert, "Got self-signed cert"); + let port = startServer(cert); + storeCertOverride(port, cert); + await startClient(port); + await startClient(port); +}); diff --git a/netwerk/test/unit/test_traceable_channel.js b/netwerk/test/unit/test_traceable_channel.js new file mode 100644 index 0000000000..9c24fb2407 --- /dev/null +++ b/netwerk/test/unit/test_traceable_channel.js @@ -0,0 +1,143 @@ +"use strict"; + +// Test nsITraceableChannel interface. +// Replace original listener with TracingListener that modifies body of HTTP +// response. Make sure that body received by original channel's listener +// is correctly modified. + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +httpserver.start(-1); +const PORT = httpserver.identity.primaryPort; + +var pipe = null; +var streamSink = null; + +var originalBody = "original http response body"; +var gotOnStartRequest = false; + +function TracingListener() {} + +TracingListener.prototype = { + onStartRequest(request) { + dump("*** tracing listener onStartRequest\n"); + + gotOnStartRequest = true; + + request.QueryInterface(Ci.nsIHttpChannelInternal); + + // local/remote addresses broken in e10s: disable for now + Assert.equal(request.localAddress, "127.0.0.1"); + Assert.equal(request.localPort > 0, true); + Assert.notEqual(request.localPort, PORT); + Assert.equal(request.remoteAddress, "127.0.0.1"); + Assert.equal(request.remotePort, PORT); + + // Make sure listener can't be replaced after OnStartRequest was called. + request.QueryInterface(Ci.nsITraceableChannel); + try { + var newListener = new TracingListener(); + newListener.listener = request.setNewListener(newListener); + } catch (e) { + dump("TracingListener.onStartRequest swallowing exception: " + e + "\n"); + return; // OK + } + do_throw("replaced channel's listener during onStartRequest."); + }, + + onStopRequest(request, statusCode) { + dump("*** tracing listener onStopRequest\n"); + + Assert.equal(gotOnStartRequest, true); + + try { + var sin = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + + streamSink.close(); + var input = pipe.inputStream; + sin.init(input); + Assert.equal(sin.available(), originalBody.length); + + var result = sin.read(originalBody.length); + Assert.equal(result, originalBody); + + input.close(); + } catch (e) { + dump("TracingListener.onStopRequest swallowing exception: " + e + "\n"); + } finally { + httpserver.stop(do_test_finished); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]), + + listener: null, +}; + +function HttpResponseExaminer() {} + +HttpResponseExaminer.prototype = { + register() { + Services.obs.addObserver(this, "http-on-examine-response", true); + dump("Did HttpResponseExaminer.register\n"); + }, + + // Replace channel's listener. + observe(subject, topic, data) { + dump("In HttpResponseExaminer.observe\n"); + try { + subject.QueryInterface(Ci.nsITraceableChannel); + + var tee = Cc["@mozilla.org/network/stream-listener-tee;1"].createInstance( + Ci.nsIStreamListenerTee + ); + var newListener = new TracingListener(); + pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(false, false, 0, 0xffffffff, null); + streamSink = pipe.outputStream; + + var originalListener = subject.setNewListener(tee); + tee.init(originalListener, streamSink, newListener); + } catch (e) { + do_throw("can't replace listener " + e); + } + dump("Did HttpResponseExaminer.observe\n"); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), +}; + +function test_handler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(originalBody, originalBody.length); +} + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +// Check if received body is correctly modified. +function channel_finished(request, input, ctx) { + httpserver.stop(do_test_finished); +} + +function run_test() { + var observer = new HttpResponseExaminer(); + observer.register(); + + httpserver.registerPathHandler("/testdir", test_handler); + + var channel = make_channel("http://localhost:" + PORT + "/testdir"); + channel.asyncOpen(new ChannelListener(channel_finished)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_trackingProtection_annotateChannels.js b/netwerk/test/unit/test_trackingProtection_annotateChannels.js new file mode 100644 index 0000000000..235f7f7ab3 --- /dev/null +++ b/netwerk/test/unit/test_trackingProtection_annotateChannels.js @@ -0,0 +1,389 @@ +"use strict"; + +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// This test supports both e10s and non-e10s mode. In non-e10s mode, this test +// drives itself by creating a profile directory, setting up the URL classifier +// test tables and adjusting the prefs which are necessary to do the testing. +// In e10s mode however, some of these operations such as creating a profile +// directory, setting up the URL classifier test tables and setting prefs +// aren't supported in the content process, so we split the test into two +// parts, the part testing the normal priority case by setting both prefs to +// false (test_trackingProtection_annotateChannels_wrap1.js), and the part +// testing the lowest priority case by setting both prefs to true +// (test_trackingProtection_annotateChannels_wrap2.js). These wrapper scripts +// are also in charge of creating the profile directory and setting up the URL +// classifier test tables. +// +// Below where we need to take different actions based on the process type we're +// in, we use runtime.processType to take the correct actions. + +const runtime = Services.appinfo; +if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT) { + do_get_profile(); +} + +const defaultTopWindowURI = NetUtil.newURI("http://www.example.com/"); + +function listener(tracking, priority, throttleable, nextTest) { + this._tracking = tracking; + this._priority = priority; + this._throttleable = throttleable; + this._nextTest = nextTest; +} +listener.prototype = { + onStartRequest(request) { + Assert.equal( + request + .QueryInterface(Ci.nsIClassifiedChannel) + .isThirdPartyTrackingResource(), + this._tracking, + "tracking flag" + ); + Assert.equal( + request.QueryInterface(Ci.nsISupportsPriority).priority, + this._priority, + "channel priority" + ); + if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT && this._tracking) { + Assert.equal( + !!( + request.QueryInterface(Ci.nsIClassOfService).classFlags & + Ci.nsIClassOfService.Throttleable + ), + this._throttleable, + "throttleable flag" + ); + } + request.cancel(Cr.NS_ERROR_ABORT); + this._nextTest(); + }, + onDataAvailable: (request, stream, offset, count) => {}, + onStopRequest: (request, status) => {}, +}; + +var httpServer; +var normalOrigin, trackingOrigin; +var testPriorityMap; +var currentTest; +// When this test is running in e10s mode, the parent process is in charge of +// setting the prefs for us, so here we merely read our prefs, and if they have +// been set we skip the normal priority test and only test the lowest priority +// case, and if it they have not been set we skip the lowest priority test and +// only test the normal priority case. +// In non-e10s mode, both of these will remain false and we adjust the prefs +// ourselves and test both of the cases in one go. +var skipNormalPriority = false, + skipLowestPriority = false; + +function setup_test() { + httpServer = new HttpServer(); + httpServer.start(-1); + httpServer.identity.setPrimary( + "http", + "tracking.example.org", + httpServer.identity.primaryPort + ); + httpServer.identity.add( + "http", + "example.org", + httpServer.identity.primaryPort + ); + normalOrigin = "http://localhost:" + httpServer.identity.primaryPort; + trackingOrigin = + "http://tracking.example.org:" + httpServer.identity.primaryPort; + + if (runtime.processType == runtime.PROCESS_TYPE_CONTENT) { + if ( + Services.prefs.getBoolPref( + "privacy.trackingprotection.annotate_channels" + ) && + Services.prefs.getBoolPref( + "privacy.trackingprotection.lower_network_priority" + ) + ) { + skipNormalPriority = true; + } else { + skipLowestPriority = true; + } + } + + runTests(); +} + +function doPriorityTest() { + if (!testPriorityMap.length) { + runTests(); + return; + } + + currentTest = testPriorityMap.shift(); + + // Let's be explicit about what we're testing! + Assert.ok( + "loadingPrincipal" in currentTest, + "check for incomplete test case" + ); + Assert.ok("topWindowURI" in currentTest, "check for incomplete test case"); + + var channel = makeChannel( + currentTest.path, + currentTest.loadingPrincipal, + currentTest.topWindowURI + ); + channel.asyncOpen( + new listener( + currentTest.expectedTracking, + currentTest.expectedPriority, + currentTest.expectedThrottleable, + doPriorityTest + ) + ); +} + +function makeChannel(path, loadingPrincipal, topWindowURI) { + var chan; + + if (loadingPrincipal) { + chan = NetUtil.newChannel({ + uri: path, + loadingPrincipal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + } else { + chan = NetUtil.newChannel({ + uri: path, + loadUsingSystemPrincipal: true, + }); + } + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + if (topWindowURI) { + chan + .QueryInterface(Ci.nsIHttpChannelInternal) + .setTopWindowURIIfUnknown(topWindowURI); + } + return chan; +} + +var tests = [ + // Create the HTTP server. + setup_test, + + // Add the test table into tracking protection table. + function addTestTrackers() { + if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT) { + UrlClassifierTestUtils.addTestTrackers().then(() => { + runTests(); + }); + } else { + runTests(); + } + }, + + // Annotations OFF, normal loading principal, topWinURI of example.com + // => trackers should not be de-prioritized + function setupAnnotationsOff() { + if (skipNormalPriority) { + runTests(); + return; + } + if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT) { + Services.prefs.setBoolPref( + "privacy.trackingprotection.annotate_channels", + false + ); + Services.prefs.setBoolPref( + "privacy.trackingprotection.lower_network_priority", + false + ); + } + var principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + normalOrigin + ); + testPriorityMap = [ + { + path: normalOrigin + "/innocent.css", + loadingPrincipal: principal, + topWindowURI: defaultTopWindowURI, + expectedTracking: false, + expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + expectedThrottleable: false, // ignored since tracking==false + }, + { + path: normalOrigin + "/innocent.js", + loadingPrincipal: principal, + topWindowURI: defaultTopWindowURI, + expectedTracking: false, + expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + expectedThrottleable: false, // ignored since tracking==false + }, + { + path: trackingOrigin + "/evil.css", + loadingPrincipal: principal, + topWindowURI: defaultTopWindowURI, + expectedTracking: false, + expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + expectedThrottleable: false, // ignored since tracking==false + }, + { + path: trackingOrigin + "/evil.js", + loadingPrincipal: principal, + topWindowURI: defaultTopWindowURI, + expectedTracking: false, + expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + expectedThrottleable: false, // ignored since tracking==false + }, + ]; + // We add the doPriorityTest test here so that it only gets injected in the + // test list if we're not skipping over this test. + tests.unshift(doPriorityTest); + runTests(); + }, + + // Annotations ON, normal loading principal, topWinURI of example.com + // => trackers should be de-prioritized + function setupAnnotationsOn() { + if (skipLowestPriority) { + runTests(); + return; + } + if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT) { + Services.prefs.setBoolPref( + "privacy.trackingprotection.annotate_channels", + true + ); + Services.prefs.setBoolPref( + "privacy.trackingprotection.lower_network_priority", + true + ); + } + var principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + normalOrigin + ); + testPriorityMap = [ + { + path: normalOrigin + "/innocent.css", + loadingPrincipal: principal, + topWindowURI: defaultTopWindowURI, + expectedTracking: false, + expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + expectedThrottleable: false, // ignored since tracking==false + }, + { + path: normalOrigin + "/innocent.js", + loadingPrincipal: principal, + topWindowURI: defaultTopWindowURI, + expectedTracking: false, + expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + expectedThrottleable: false, // ignored since tracking==false + }, + { + path: trackingOrigin + "/evil.css", + loadingPrincipal: principal, + topWindowURI: defaultTopWindowURI, + expectedTracking: true, + expectedPriority: Ci.nsISupportsPriority.PRIORITY_LOWEST, + expectedThrottleable: true, + }, + { + path: trackingOrigin + "/evil.js", + loadingPrincipal: principal, + topWindowURI: defaultTopWindowURI, + expectedTracking: true, + expectedPriority: Ci.nsISupportsPriority.PRIORITY_LOWEST, + expectedThrottleable: true, + }, + ]; + // We add the doPriorityTest test here so that it only gets injected in the + // test list if we're not skipping over this test. + tests.unshift(doPriorityTest); + runTests(); + }, + + // Annotations ON, system loading principal, topWinURI of example.com + // => trackers should not be de-prioritized + function setupAnnotationsOnSystemPrincipal() { + if (skipLowestPriority) { + runTests(); + return; + } + if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT) { + Services.prefs.setBoolPref( + "privacy.trackingprotection.annotate_channels", + true + ); + Services.prefs.setBoolPref( + "privacy.trackingprotection.lower_network_priority", + true + ); + } + testPriorityMap = [ + { + path: normalOrigin + "/innocent.css", + loadingPrincipal: null, // system principal + topWindowURI: defaultTopWindowURI, + expectedTracking: false, + expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + expectedThrottleable: false, // ignored since tracking==false + }, + { + path: normalOrigin + "/innocent.js", + loadingPrincipal: null, // system principal + topWindowURI: defaultTopWindowURI, + expectedTracking: false, + expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + expectedThrottleable: false, // ignored since tracking==false + }, + { + path: trackingOrigin + "/evil.css", + loadingPrincipal: null, // system principal + topWindowURI: defaultTopWindowURI, + expectedTracking: false, + expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + expectedThrottleable: false, + }, + { + path: trackingOrigin + "/evil.js", + loadingPrincipal: null, // system principal + topWindowURI: defaultTopWindowURI, + expectedTracking: false, + expectedPriority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + expectedThrottleable: true, + }, + ]; + // We add the doPriorityTest test here so that it only gets injected in the + // test list if we're not skipping over this test. + tests.unshift(doPriorityTest); + runTests(); + }, + + function cleanUp() { + httpServer.stop(do_test_finished); + if (runtime.processType == runtime.PROCESS_TYPE_DEFAULT) { + UrlClassifierTestUtils.cleanupTestTrackers(); + } + runTests(); + }, +]; + +function runTests() { + if (!tests.length) { + do_test_finished(); + return; + } + + var test = tests.shift(); + test(); +} + +function run_test() { + runTests(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_trr.js b/netwerk/test/unit/test_trr.js new file mode 100644 index 0000000000..1fcba44f86 --- /dev/null +++ b/netwerk/test/unit/test_trr.js @@ -0,0 +1,902 @@ +"use strict"; + +/* import-globals-from trr_common.js */ + +const gDefaultPref = Services.prefs.getDefaultBranch(""); + +SetParentalControlEnabled(false); + +function setup() { + h2Port = trr_test_setup(); +} + +setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); +}); + +async function waitForConfirmation(expectedResponseIP, confirmationShouldFail) { + // Check that the confirmation eventually completes. + let count = 100; + while (count > 0) { + if (count == 50 || count == 10) { + // At these two points we do a longer timeout to account for a slow + // response on the server side. This is usually a problem on the Android + // because of the increased delay between the emulator and host. + await new Promise(resolve => do_timeout(100 * (100 / count), resolve)); + } + let { inRecord } = await new TRRDNSListener( + `ip${count}.example.org`, + undefined, + false + ); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + let responseIP = inRecord.getNextAddrAsString(); + Assert.ok(true, responseIP); + if (responseIP == expectedResponseIP) { + break; + } + count--; + } + + if (confirmationShouldFail) { + Assert.equal(count, 0, "Confirmation did not finish after 100 iterations"); + return; + } + + Assert.greater(count, 0, "Finished confirmation before 100 iterations"); +} + +function setModeAndURI(mode, path) { + Services.prefs.setIntPref("network.trr.mode", mode); + Services.prefs.setCharPref( + "network.trr.uri", + `https://${TRR_Domain}:${h2Port}/${path}` + ); +} + +function makeChan(url, mode, bypassCache) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + chan.setTRRMode(mode); + return chan; +} + +add_task(async function test_server_up() { + // This test checks that moz-http2.js running in node is working. + // This should always be the first test in this file (except for setup) + // otherwise we may encounter random failures when the http2 server is down. + + await NodeServer.execute("bad_id", `"hello"`) + .then(() => ok(false, "expecting to throw")) + .catch(e => equal(e.message, "Error: could not find id")); +}); + +add_task(async function test_trr_flags() { + Services.prefs.setBoolPref("network.trr.fallback-on-zero-response", true); + + let httpserv = new HttpServer(); + httpserv.registerPathHandler("/", function handler(metadata, response) { + let content = "ok"; + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + }); + httpserv.start(-1); + + const URL = `http://example.com:${httpserv.identity.primaryPort}/`; + + for (let mode of [0, 1, 2, 3, 4, 5]) { + setModeAndURI(mode, "doh?responseIP=127.0.0.1"); + for (let flag of [ + Ci.nsIRequest.TRR_DEFAULT_MODE, + Ci.nsIRequest.TRR_DISABLED_MODE, + Ci.nsIRequest.TRR_FIRST_MODE, + Ci.nsIRequest.TRR_ONLY_MODE, + ]) { + Services.dns.clearCache(true); + let chan = makeChan(URL, flag); + let expectTRR = + ([2, 3].includes(mode) && flag != Ci.nsIRequest.TRR_DISABLED_MODE) || + (mode == 0 && + [Ci.nsIRequest.TRR_FIRST_MODE, Ci.nsIRequest.TRR_ONLY_MODE].includes( + flag + )); + + await new Promise(resolve => + chan.asyncOpen(new ChannelListener(resolve)) + ); + + equal(chan.getTRRMode(), flag); + equal( + expectTRR, + chan.QueryInterface(Ci.nsIHttpChannelInternal).isResolvedByTRR + ); + } + } + + await new Promise(resolve => httpserv.stop(resolve)); + Services.prefs.clearUserPref("network.trr.fallback-on-zero-response"); +}); + +add_task(test_A_record); + +add_task(async function test_push() { + info("Verify DOH push"); + Services.dns.clearCache(true); + info("Asking server to push us a record"); + setModeAndURI(3, "doh?responseIP=5.5.5.5&push=true"); + + await new TRRDNSListener("first.example.com", "5.5.5.5"); + + // At this point the second host name should've been pushed and we can resolve it using + // cache only. Set back the URI to a path that fails. + // Don't clear the cache, otherwise we lose the pushed record. + setModeAndURI(3, "404"); + + await new TRRDNSListener("push.example.org", "2018::2018"); +}); + +add_task(test_AAAA_records); + +add_task(test_RFC1918); + +add_task(test_GET_ECS); + +add_task(test_timeout_mode3); + +add_task(test_trr_retry); + +add_task(test_strict_native_fallback); + +add_task(test_no_answers_fallback); + +add_task(test_404_fallback); + +add_task(test_mode_1_and_4); + +add_task(test_CNAME); + +add_task(test_name_mismatch); + +add_task(test_mode_2); + +add_task(test_excluded_domains); + +add_task(test_captiveportal_canonicalURL); + +add_task(test_parentalcontrols); + +// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref +add_task(test_builtin_excluded_domains); + +add_task(test_excluded_domains_mode3); + +add_task(test25e); + +add_task(test_parentalcontrols_mode3); + +add_task(test_builtin_excluded_domains_mode3); + +add_task(count_cookies); + +add_task(test_connection_closed); + +add_task(async function test_clearCacheOnURIChange() { + info("Check that the TRR cache should be cleared by a pref change."); + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", true); + setModeAndURI(2, "doh?responseIP=7.7.7.7"); + + await new TRRDNSListener("bar.example.com", "7.7.7.7"); + + // The TRR cache should be cleared by this pref change. + Services.prefs.setCharPref( + "network.trr.uri", + `https://localhost:${h2Port}/doh?responseIP=8.8.8.8` + ); + + await new TRRDNSListener("bar.example.com", "8.8.8.8"); + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); +}); + +add_task(async function test_dnsSuffix() { + info("Checking that domains matching dns suffix list use Do53"); + async function checkDnsSuffixInMode(mode) { + Services.dns.clearCache(true); + setModeAndURI(mode, "doh?responseIP=1.2.3.4&push=true"); + await new TRRDNSListener("example.org", "1.2.3.4"); + await new TRRDNSListener("push.example.org", "2018::2018"); + await new TRRDNSListener("test.com", "1.2.3.4"); + + let networkLinkService = { + dnsSuffixList: ["example.org"], + QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]), + }; + Services.obs.notifyObservers( + networkLinkService, + "network:dns-suffix-list-updated" + ); + await new TRRDNSListener("test.com", "1.2.3.4"); + if (Services.prefs.getBoolPref("network.trr.split_horizon_mitigations")) { + await new TRRDNSListener("example.org", "127.0.0.1"); + // Also test that we don't use the pushed entry. + await new TRRDNSListener("push.example.org", "127.0.0.1"); + } else { + await new TRRDNSListener("example.org", "1.2.3.4"); + await new TRRDNSListener("push.example.org", "2018::2018"); + } + + // Attempt to clean up, just in case + networkLinkService.dnsSuffixList = []; + Services.obs.notifyObservers( + networkLinkService, + "network:dns-suffix-list-updated" + ); + } + + Services.prefs.setBoolPref("network.trr.split_horizon_mitigations", true); + await checkDnsSuffixInMode(2); + Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1"); + await checkDnsSuffixInMode(3); + Services.prefs.setBoolPref("network.trr.split_horizon_mitigations", false); + // Test again with mitigations off + await checkDnsSuffixInMode(2); + await checkDnsSuffixInMode(3); + Services.prefs.clearUserPref("network.trr.split_horizon_mitigations"); + Services.prefs.clearUserPref("network.trr.bootstrapAddr"); +}); + +add_task(async function test_async_resolve_with_trr_server() { + info("Checking asyncResolveWithTrrServer"); + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 0); // TRR-disabled + + await new TRRDNSListener( + "bar_with_trr1.example.com", + "2.2.2.2", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + + // Test request without trr server, it should return a native dns response. + await new TRRDNSListener("bar_with_trr1.example.com", "127.0.0.1"); + + // Mode 2 + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + + await new TRRDNSListener( + "bar_with_trr2.example.com", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); + + // Test request without trr server, it should return a response from trr server defined in the pref. + await new TRRDNSListener("bar_with_trr2.example.com", "2.2.2.2"); + + // Mode 3 + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=2.2.2.2"); + + await new TRRDNSListener( + "bar_with_trr3.example.com", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); + + // Test request without trr server, it should return a response from trr server defined in the pref. + await new TRRDNSListener("bar_with_trr3.example.com", "2.2.2.2"); + + // Mode 5 + Services.dns.clearCache(true); + setModeAndURI(5, "doh?responseIP=2.2.2.2"); + + // When dns is resolved in socket process, we can't set |expectEarlyFail| to true. + let inSocketProcess = mozinfo.socketprocess_networking; + await new TRRDNSListener( + "bar_with_trr3.example.com", + undefined, + false, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`, + !inSocketProcess + ); + + // Call normal AsyncOpen, it will return result from the native resolver. + await new TRRDNSListener("bar_with_trr3.example.com", "127.0.0.1"); + + // Check that cache is ignored when server is different + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=2.2.2.2"); + + await new TRRDNSListener("bar_with_trr4.example.com", "2.2.2.2", true); + + // The record will be fetch again. + await new TRRDNSListener( + "bar_with_trr4.example.com", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); + + // The record will be fetch again. + await new TRRDNSListener( + "bar_with_trr5.example.com", + "4.4.4.4", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=4.4.4.4` + ); + + // Check no fallback and no blocklisting upon failure + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + + let { inStatus } = await new TRRDNSListener( + "bar_with_trr6.example.com", + undefined, + false, + undefined, + `https://foo.example.com:${h2Port}/404` + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + await new TRRDNSListener("bar_with_trr6.example.com", "2.2.2.2", true); + + // Check that DoH push doesn't work + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + + await new TRRDNSListener( + "bar_with_trr7.example.com", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3&push=true` + ); + + // AsyncResoleWithTrrServer rejects server pushes and the entry for push.example.org + // shouldn't be neither in the default cache not in AsyncResoleWithTrrServer cache. + setModeAndURI(2, "404"); + + await new TRRDNSListener( + "push.example.org", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3&push=true` + ); + + await new TRRDNSListener("push.example.org", "127.0.0.1"); + + // Check confirmation is ignored + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=1::ffff"); + Services.prefs.clearUserPref("network.trr.useGET"); + Services.prefs.clearUserPref("network.trr.disable-ECS"); + Services.prefs.setCharPref( + "network.trr.confirmationNS", + "confirm.example.com" + ); + + // AsyncResoleWithTrrServer will succeed + await new TRRDNSListener( + "bar_with_trr8.example.com", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); + + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + + // Bad port + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + + ({ inStatus } = await new TRRDNSListener( + "only_once.example.com", + undefined, + false, + undefined, + `https://target.example.com:666/404` + )); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + // // MOZ_LOG=sync,timestamp,nsHostResolver:5 We should not keep resolving only_once.example.com + // // TODO: find a way of automating this + // await new Promise(resolve => {}); +}); + +add_task(test_fetch_time); + +add_task(async function test_content_encoding_gzip() { + info("Checking gzip content encoding"); + Services.dns.clearCache(true); + Services.prefs.setBoolPref( + "network.trr.send_empty_accept-encoding_headers", + false + ); + setModeAndURI(3, "doh?responseIP=2.2.2.2"); + + await new TRRDNSListener("bar.example.com", "2.2.2.2"); + Services.prefs.clearUserPref( + "network.trr.send_empty_accept-encoding_headers" + ); +}); + +add_task(async function test_redirect() { + info("Check handling of redirect"); + + // GET + Services.dns.clearCache(true); + setModeAndURI(3, "doh?redirect=4.4.4.4{&dns}"); + Services.prefs.setBoolPref("network.trr.useGET", true); + Services.prefs.setBoolPref("network.trr.disable-ECS", true); + + await new TRRDNSListener("ecs.example.com", "4.4.4.4"); + + // POST + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.useGET", false); + setModeAndURI(3, "doh?redirect=4.4.4.4"); + + await new TRRDNSListener("bar.example.com", "4.4.4.4"); + + Services.prefs.clearUserPref("network.trr.useGET"); + Services.prefs.clearUserPref("network.trr.disable-ECS"); +}); + +// confirmationNS set without confirmed NS yet +// checks that we properly fall back to DNS is confirmation is not ready yet, +// and wait-for-confirmation pref is true +add_task(async function test_confirmation() { + info("Checking that we fall back correctly when confirmation is pending"); + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.wait-for-confirmation", true); + setModeAndURI(2, "doh?responseIP=7.7.7.7&slowConfirm=true"); + Services.prefs.setCharPref( + "network.trr.confirmationNS", + "confirm.example.com" + ); + + await new TRRDNSListener("example.org", "127.0.0.1"); + await new Promise(resolve => do_timeout(1000, resolve)); + await waitForConfirmation("7.7.7.7"); + + // Reset between each test to force re-confirm + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + + info("Check that confirmation is skipped in mode 3"); + // This is just a smoke test to make sure lookups succeed immediately + // in mode 3 without waiting for confirmation. + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=1::ffff&slowConfirm=true"); + Services.prefs.setCharPref( + "network.trr.confirmationNS", + "confirm.example.com" + ); + + await new TRRDNSListener("skipConfirmationForMode3.example.com", "1::ffff"); + + // Reset between each test to force re-confirm + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.wait-for-confirmation", false); + setModeAndURI(2, "doh?responseIP=7.7.7.7&slowConfirm=true"); + Services.prefs.setCharPref( + "network.trr.confirmationNS", + "confirm.example.com" + ); + + // DoH available immediately + await new TRRDNSListener("example.org", "7.7.7.7"); + + // Reset between each test to force re-confirm + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + + // Fallback when confirmation fails + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.wait-for-confirmation", true); + setModeAndURI(2, "404"); + Services.prefs.setCharPref( + "network.trr.confirmationNS", + "confirm.example.com" + ); + + await waitForConfirmation("7.7.7.7", true); + + await new TRRDNSListener("example.org", "127.0.0.1"); + + // Reset + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + Services.prefs.clearUserPref("network.trr.wait-for-confirmation"); +}); + +add_task(test_fqdn); + +add_task(async function test_detected_uri() { + info("Test setDetectedTrrURI"); + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.clearUserPref("network.trr.uri"); + let defaultURI = gDefaultPref.getCharPref("network.trr.default_provider_uri"); + gDefaultPref.setCharPref( + "network.trr.default_provider_uri", + `https://foo.example.com:${h2Port}/doh?responseIP=3.4.5.6` + ); + await new TRRDNSListener("domainA.example.org.", "3.4.5.6"); + Services.dns.setDetectedTrrURI( + `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4` + ); + await new TRRDNSListener("domainB.example.org.", "1.2.3.4"); + gDefaultPref.setCharPref("network.trr.default_provider_uri", defaultURI); + + // With a user-set doh uri this time. + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=4.5.6.7"); + await new TRRDNSListener("domainA.example.org.", "4.5.6.7"); + + // This should be a no-op, since we have a user-set URI + Services.dns.setDetectedTrrURI( + `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4` + ); + await new TRRDNSListener("domainB.example.org.", "4.5.6.7"); + + // Test network link status change + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.clearUserPref("network.trr.uri"); + gDefaultPref.setCharPref( + "network.trr.default_provider_uri", + `https://foo.example.com:${h2Port}/doh?responseIP=3.4.5.6` + ); + await new TRRDNSListener("domainA.example.org.", "3.4.5.6"); + Services.dns.setDetectedTrrURI( + `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4` + ); + await new TRRDNSListener("domainB.example.org.", "1.2.3.4"); + + let networkLinkService = { + platformDNSIndications: 0, + QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]), + }; + + Services.obs.notifyObservers( + networkLinkService, + "network:link-status-changed", + "changed" + ); + + await new TRRDNSListener("domainC.example.org.", "3.4.5.6"); + + gDefaultPref.setCharPref("network.trr.default_provider_uri", defaultURI); +}); + +add_task(async function test_pref_changes() { + info("Testing pref change handling"); + Services.prefs.clearUserPref("network.trr.uri"); + let defaultURI = gDefaultPref.getCharPref("network.trr.default_provider_uri"); + + async function doThenCheckURI(closure, expectedURI, expectChange = true) { + let uriChanged; + if (expectChange) { + uriChanged = topicObserved("network:trr-uri-changed"); + } + closure(); + if (expectChange) { + await uriChanged; + } + equal(Services.dns.currentTrrURI, expectedURI); + } + + // setting the default value of the pref should be reflected in the URI + await doThenCheckURI(() => { + gDefaultPref.setCharPref( + "network.trr.default_provider_uri", + `https://foo.example.com:${h2Port}/doh?default` + ); + }, `https://foo.example.com:${h2Port}/doh?default`); + + // the user set value should be reflected in the URI + await doThenCheckURI(() => { + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?user` + ); + }, `https://foo.example.com:${h2Port}/doh?user`); + + // A user set pref is selected, so it should be chosen instead of the rollout + await doThenCheckURI( + () => { + Services.prefs.setCharPref( + "doh-rollout.uri", + `https://foo.example.com:${h2Port}/doh?rollout` + ); + }, + `https://foo.example.com:${h2Port}/doh?user`, + false + ); + + // There is no user set pref, so we go to the rollout pref + await doThenCheckURI(() => { + Services.prefs.clearUserPref("network.trr.uri"); + }, `https://foo.example.com:${h2Port}/doh?rollout`); + + // When the URI is set by the rollout addon, detection is allowed + await doThenCheckURI(() => { + Services.dns.setDetectedTrrURI( + `https://foo.example.com:${h2Port}/doh?detected` + ); + }, `https://foo.example.com:${h2Port}/doh?detected`); + + // Should switch back to the default provided by the rollout addon + await doThenCheckURI(() => { + let networkLinkService = { + platformDNSIndications: 0, + QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]), + }; + Services.obs.notifyObservers( + networkLinkService, + "network:link-status-changed", + "changed" + ); + }, `https://foo.example.com:${h2Port}/doh?rollout`); + + // Again the user set pref should be chosen + await doThenCheckURI(() => { + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?user` + ); + }, `https://foo.example.com:${h2Port}/doh?user`); + + // Detection should not work with a user set pref + await doThenCheckURI( + () => { + Services.dns.setDetectedTrrURI( + `https://foo.example.com:${h2Port}/doh?detected` + ); + }, + `https://foo.example.com:${h2Port}/doh?user`, + false + ); + + // Should stay the same on network changes + await doThenCheckURI( + () => { + let networkLinkService = { + platformDNSIndications: 0, + QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]), + }; + Services.obs.notifyObservers( + networkLinkService, + "network:link-status-changed", + "changed" + ); + }, + `https://foo.example.com:${h2Port}/doh?user`, + false + ); + + // Restore the pref + gDefaultPref.setCharPref("network.trr.default_provider_uri", defaultURI); +}); + +add_task(async function test_dohrollout_mode() { + info("Testing doh-rollout.mode"); + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("doh-rollout.mode"); + + equal(Services.dns.currentTrrMode, 0); + + async function doThenCheckMode(trrMode, rolloutMode, expectedMode, message) { + let modeChanged; + if (Services.dns.currentTrrMode != expectedMode) { + modeChanged = topicObserved("network:trr-mode-changed"); + } + + if (trrMode != undefined) { + Services.prefs.setIntPref("network.trr.mode", trrMode); + } + + if (rolloutMode != undefined) { + Services.prefs.setIntPref("doh-rollout.mode", rolloutMode); + } + + if (modeChanged) { + await modeChanged; + } + equal(Services.dns.currentTrrMode, expectedMode, message); + } + + await doThenCheckMode(2, undefined, 2); + await doThenCheckMode(3, undefined, 3); + await doThenCheckMode(5, undefined, 5); + await doThenCheckMode(2, undefined, 2); + await doThenCheckMode(0, undefined, 0); + await doThenCheckMode(1, undefined, 5); + await doThenCheckMode(6, undefined, 5); + + await doThenCheckMode(2, 0, 2); + await doThenCheckMode(2, 1, 2); + await doThenCheckMode(2, 2, 2); + await doThenCheckMode(2, 3, 2); + await doThenCheckMode(2, 5, 2); + await doThenCheckMode(3, 2, 3); + await doThenCheckMode(5, 2, 5); + + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("doh-rollout.mode"); + + await doThenCheckMode(undefined, 2, 2); + await doThenCheckMode(undefined, 3, 3); + + // All modes that are not 0,2,3 are treated as 5 + await doThenCheckMode(undefined, 5, 5); + await doThenCheckMode(undefined, 4, 5); + await doThenCheckMode(undefined, 6, 5); + + await doThenCheckMode(undefined, 2, 2); + await doThenCheckMode(3, undefined, 3); + + Services.prefs.clearUserPref("network.trr.mode"); + equal(Services.dns.currentTrrMode, 2); + Services.prefs.clearUserPref("doh-rollout.mode"); + equal(Services.dns.currentTrrMode, 0); +}); + +add_task(test_ipv6_trr_fallback); + +add_task(test_ipv4_trr_fallback); + +add_task(test_no_retry_without_doh); + +// This test checks that normally when the TRR mode goes from ON -> OFF +// we purge the DNS cache (including TRR), so the entries aren't used on +// networks where they shouldn't. For example - turning on a VPN. +add_task(async function test_purge_trr_cache_on_mode_change() { + info("Checking that we purge cache when TRR is turned off"); + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", true); + + Services.prefs.setIntPref("network.trr.mode", 0); + Services.prefs.setIntPref("doh-rollout.mode", 2); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); + + await new TRRDNSListener("cached.example.com", "3.3.3.3"); + Services.prefs.clearUserPref("doh-rollout.mode"); + + await new TRRDNSListener("cached.example.com", "127.0.0.1"); + + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + Services.prefs.clearUserPref("doh-rollout.mode"); +}); + +add_task(async function test_old_bootstrap_pref() { + Services.dns.clearCache(true); + // Note this is a remote address. Setting this pref should have no effect, + // as this is the old name for the bootstrap pref. + // If this were to be used, the test would crash when accessing a non-local + // IP address. + Services.prefs.setCharPref("network.trr.bootstrapAddress", "1.1.1.1"); + setModeAndURI(Ci.nsIDNSService.MODE_TRRONLY, `doh?responseIP=4.4.4.4`); + await new TRRDNSListener("testytest.com", "4.4.4.4"); +}); + +add_task(async function test_padding() { + setModeAndURI(Ci.nsIDNSService.MODE_TRRONLY, `doh`); + async function CheckPadding( + pad_length, + request, + none, + ecs, + padding, + ecsPadding + ) { + Services.prefs.setIntPref("network.trr.padding.length", pad_length); + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.padding", false); + Services.prefs.setBoolPref("network.trr.disable-ECS", false); + await new TRRDNSListener(request, none); + + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.padding", false); + Services.prefs.setBoolPref("network.trr.disable-ECS", true); + await new TRRDNSListener(request, ecs); + + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.padding", true); + Services.prefs.setBoolPref("network.trr.disable-ECS", false); + await new TRRDNSListener(request, padding); + + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.padding", true); + Services.prefs.setBoolPref("network.trr.disable-ECS", true); + await new TRRDNSListener(request, ecsPadding); + } + + // short domain name + await CheckPadding( + 16, + "a.pd", + "2.2.0.22", + "2.2.0.41", + "1.1.0.48", + "1.1.0.48" + ); + await CheckPadding(256, "a.pd", "2.2.0.22", "2.2.0.41", "1.1.1.0", "1.1.1.0"); + + // medium domain name + await CheckPadding( + 16, + "has-padding.pd", + "2.2.0.32", + "2.2.0.51", + "1.1.0.48", + "1.1.0.64" + ); + await CheckPadding( + 128, + "has-padding.pd", + "2.2.0.32", + "2.2.0.51", + "1.1.0.128", + "1.1.0.128" + ); + await CheckPadding( + 80, + "has-padding.pd", + "2.2.0.32", + "2.2.0.51", + "1.1.0.80", + "1.1.0.80" + ); + + // long domain name + await CheckPadding( + 16, + "abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.pd", + "2.2.0.131", + "2.2.0.150", + "1.1.0.160", + "1.1.0.160" + ); + await CheckPadding( + 128, + "abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.pd", + "2.2.0.131", + "2.2.0.150", + "1.1.1.0", + "1.1.1.0" + ); + await CheckPadding( + 80, + "abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.pd", + "2.2.0.131", + "2.2.0.150", + "1.1.0.160", + "1.1.0.160" + ); +}); + +add_task(test_connection_reuse_and_cycling); diff --git a/netwerk/test/unit/test_trr_additional_section.js b/netwerk/test/unit/test_trr_additional_section.js new file mode 100644 index 0000000000..37e3573e34 --- /dev/null +++ b/netwerk/test/unit/test_trr_additional_section.js @@ -0,0 +1,337 @@ +/* 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"; + +trr_test_setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish)); + }); +} + +let trrServer = new TRRServer(); +registerCleanupFunction(async () => { + await trrServer.stop(); +}); +add_task(async function setup_server() { + await trrServer.start(); + dump(`port = ${trrServer.port}\n`); + let chan = makeChan(`https://localhost:${trrServer.port}/test?bla=some`); + let [, resp] = await channelOpenPromise(chan); + equal(resp, "<h1> 404 Path not found: /test?bla=some</h1>"); +}); + +add_task(async function test_parse_additional_section() { + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("something.foo", "A", { + answers: [ + { + name: "something.foo", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + additionals: [ + { + name: "else.foo", + ttl: 55, + type: "A", + flush: false, + data: "2.3.4.5", + }, + ], + }); + + await new TRRDNSListener("something.foo", { expectedAnswer: "1.2.3.4" }); + await new TRRDNSListener("else.foo", { expectedAnswer: "2.3.4.5" }); + + await trrServer.registerDoHAnswers("a.foo", "A", { + answers: [ + { + name: "a.foo", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + additionals: [ + { + name: "b.foo", + ttl: 55, + type: "A", + flush: false, + data: "2.3.4.5", + }, + ], + }); + await trrServer.registerDoHAnswers("b.foo", "A", { + answers: [ + { + name: "b.foo", + ttl: 55, + type: "A", + flush: false, + data: "3.4.5.6", + }, + ], + }); + + let req1 = new TRRDNSListener("a.foo", { expectedAnswer: "1.2.3.4" }); + + // A request for b.foo will be in progress by the time we parse the additional + // record. To keep things simple we don't end up saving the record, instead + // we wait for the in-progress request to complete. + // This check is also racy - if the response for a.foo completes before we make + // this request, we'll put the other IP in the cache. But that is very unlikely. + let req2 = new TRRDNSListener("b.foo", { expectedAnswer: "3.4.5.6" }); + + await Promise.all([req1, req2]); + + // IPv6 additional + await trrServer.registerDoHAnswers("xyz.foo", "A", { + answers: [ + { + name: "xyz.foo", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + additionals: [ + { + name: "abc.foo", + ttl: 55, + type: "AAAA", + flush: false, + data: "::1:2:3:4", + }, + ], + }); + + await new TRRDNSListener("xyz.foo", { expectedAnswer: "1.2.3.4" }); + await new TRRDNSListener("abc.foo", { expectedAnswer: "::1:2:3:4" }); + + // IPv6 additional + await trrServer.registerDoHAnswers("ipv6.foo", "AAAA", { + answers: [ + { + name: "ipv6.foo", + ttl: 55, + type: "AAAA", + flush: false, + data: "2001::a:b:c:d", + }, + ], + additionals: [ + { + name: "def.foo", + ttl: 55, + type: "AAAA", + flush: false, + data: "::a:b:c:d", + }, + ], + }); + + await new TRRDNSListener("ipv6.foo", { expectedAnswer: "2001::a:b:c:d" }); + await new TRRDNSListener("def.foo", { expectedAnswer: "::a:b:c:d" }); + + // IPv6 additional + await trrServer.registerDoHAnswers("ipv6b.foo", "AAAA", { + answers: [ + { + name: "ipv6b.foo", + ttl: 55, + type: "AAAA", + flush: false, + data: "2001::a:b:c:d", + }, + ], + additionals: [ + { + name: "qqqq.foo", + ttl: 55, + type: "A", + flush: false, + data: "9.8.7.6", + }, + ], + }); + + await new TRRDNSListener("ipv6b.foo", { expectedAnswer: "2001::a:b:c:d" }); + await new TRRDNSListener("qqqq.foo", { expectedAnswer: "9.8.7.6" }); + + // Multiple IPs and multiple additional records + await trrServer.registerDoHAnswers("multiple.foo", "A", { + answers: [ + { + name: "multiple.foo", + ttl: 55, + type: "A", + flush: false, + data: "9.9.9.9", + }, + ], + additionals: [ + { + // Should be ignored, because it should be in the answer section + name: "multiple.foo", + ttl: 55, + type: "A", + flush: false, + data: "1.1.1.1", + }, + { + // Is ignored, because it should be in the answer section + name: "multiple.foo", + ttl: 55, + type: "AAAA", + flush: false, + data: "2001::a:b:c:d", + }, + { + name: "yuiop.foo", + ttl: 55, + type: "AAAA", + flush: false, + data: "2001::a:b:c:d", + }, + { + name: "yuiop.foo", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + + let { inRecord } = await new TRRDNSListener("multiple.foo", { + expectedAnswer: "9.9.9.9", + }); + let IPs = []; + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + inRecord.rewind(); + while (inRecord.hasMore()) { + IPs.push(inRecord.getNextAddrAsString()); + } + equal(IPs.length, 1); + equal(IPs[0], "9.9.9.9"); + IPs = []; + ({ inRecord } = await new TRRDNSListener("yuiop.foo", { + expectedSuccess: false, + })); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + inRecord.rewind(); + while (inRecord.hasMore()) { + IPs.push(inRecord.getNextAddrAsString()); + } + equal(IPs.length, 2); + equal(IPs[0], "2001::a:b:c:d"); + equal(IPs[1], "1.2.3.4"); +}); + +add_task(async function test_additional_after_resolve() { + await trrServer.registerDoHAnswers("first.foo", "A", { + answers: [ + { + name: "first.foo", + ttl: 55, + type: "A", + flush: false, + data: "3.4.5.6", + }, + ], + }); + await new TRRDNSListener("first.foo", { expectedAnswer: "3.4.5.6" }); + + await trrServer.registerDoHAnswers("second.foo", "A", { + answers: [ + { + name: "second.foo", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + additionals: [ + { + name: "first.foo", + ttl: 55, + type: "A", + flush: false, + data: "2.3.4.5", + }, + ], + }); + + await new TRRDNSListener("second.foo", { expectedAnswer: "1.2.3.4" }); + await new TRRDNSListener("first.foo", { expectedAnswer: "2.3.4.5" }); +}); + +// test for Bug - 1790075 +// Crash was observed when a DNS (using TRR) reply contains an additional +// record field and this addditional record was previously unsuccessfully +// resolved +add_task(async function test_additional_cached_record_override() { + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await new TRRDNSListener("else.foo", { expectedAnswer: "127.0.0.1" }); + + await trrServer.registerDoHAnswers("something.foo", "A", { + answers: [ + { + name: "something.foo", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + additionals: [ + { + name: "else.foo", + ttl: 55, + type: "A", + flush: false, + data: "2.3.4.5", + }, + ], + }); + + await new TRRDNSListener("something.foo", { expectedAnswer: "1.2.3.4" }); + await new TRRDNSListener("else.foo", { expectedAnswer: "2.3.4.5" }); +}); diff --git a/netwerk/test/unit/test_trr_af_fallback.js b/netwerk/test/unit/test_trr_af_fallback.js new file mode 100644 index 0000000000..4ce651424d --- /dev/null +++ b/netwerk/test/unit/test_trr_af_fallback.js @@ -0,0 +1,117 @@ +/* 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 gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); + +trr_test_setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); +}); + +let trrServer = null; +add_task(async function start_trr_server() { + trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + dump(`port = ${trrServer.port}\n`); + Services.prefs.setBoolPref("network.trr.skip-AAAA-when-not-supported", false); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); +}); + +add_task(async function unspec_first() { + gOverride.clearOverrides(); + Services.dns.clearCache(true); + + gOverride.addIPOverride("example.org", "1.1.1.1"); + gOverride.addIPOverride("example.org", "::1"); + + await trrServer.registerDoHAnswers("example.org", "A", { + answers: [ + { + name: "example.org", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + // This first request gets cached. IPv6 response gets served from the cache + await new TRRDNSListener("example.org", { expectedAnswer: "1.2.3.4" }); + await new TRRDNSListener("example.org", { + flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV6, + expectedAnswer: "1.2.3.4", + }); + let { inStatus } = await new TRRDNSListener("example.org", { + flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV4, + expectedSuccess: false, + }); + equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); +}); + +add_task(async function A_then_AAAA_fails() { + gOverride.clearOverrides(); + Services.dns.clearCache(true); + + gOverride.addIPOverride("example.org", "1.1.1.1"); + gOverride.addIPOverride("example.org", "::1"); + + await trrServer.registerDoHAnswers("example.org", "A", { + answers: [ + { + name: "example.org", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + // We do individual IPv4/IPv6 requests - we expect IPv6 not to fallback to Do53 because we have an IPv4 record + await new TRRDNSListener("example.org", { + flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV6, + expectedAnswer: "1.2.3.4", + }); + let { inStatus } = await new TRRDNSListener("example.org", { + flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV4, + expectedSuccess: false, + }); + equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); +}); + +add_task(async function just_AAAA_fails() { + gOverride.clearOverrides(); + Services.dns.clearCache(true); + + gOverride.addIPOverride("example.org", "1.1.1.1"); + gOverride.addIPOverride("example.org", "::1"); + + await trrServer.registerDoHAnswers("example.org", "A", { + answers: [ + { + name: "example.org", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + // We only do an IPv6 req - we expect IPv6 not to fallback to Do53 because we have an IPv4 record + let { inStatus } = await new TRRDNSListener("example.org", { + flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV4, + expectedSuccess: false, + }); + equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); +}); diff --git a/netwerk/test/unit/test_trr_blocklist.js b/netwerk/test/unit/test_trr_blocklist.js new file mode 100644 index 0000000000..c16b73f830 --- /dev/null +++ b/netwerk/test/unit/test_trr_blocklist.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/. */ + +"use strict"; + +const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); + +function setup() { + trr_test_setup(); + Services.prefs.setBoolPref("network.trr.temp_blocklist", true); +} +setup(); + +add_task(async function checkBlocklisting() { + let trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + info(`port = ${trrServer.port}\n`); + + Services.dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); + + await trrServer.registerDoHAnswers("top.test.com", "NS", {}); + + override.addIPOverride("sub.top.test.com", "2.2.2.2"); + override.addIPOverride("sub2.top.test.com", "2.2.2.2"); + await new TRRDNSListener("sub.top.test.com", { + expectedAnswer: "2.2.2.2", + }); + equal(await trrServer.requestCount("sub.top.test.com", "A"), 1); + + // Clear the cache so that we need to consult the blocklist and not simply + // return the cached DNS record. + Services.dns.clearCache(true); + await new TRRDNSListener("sub.top.test.com", { + expectedAnswer: "2.2.2.2", + }); + equal( + await trrServer.requestCount("sub.top.test.com", "A"), + 1, + "Request should go directly to native because result is still in blocklist" + ); + + // XXX(valentin): if this ever starts intermittently failing we need to add + // a sleep here. But the check for the parent NS should normally complete + // before the second subdomain request. + equal( + await trrServer.requestCount("top.test.com", "NS"), + 1, + "Should have checked parent domain" + ); + await new TRRDNSListener("sub2.top.test.com", { + expectedAnswer: "2.2.2.2", + }); + equal(await trrServer.requestCount("sub2.top.test.com", "A"), 0); + + // The blocklist should instantly expire. + Services.prefs.setIntPref("network.trr.temp_blocklist_duration_sec", 0); + Services.dns.clearCache(true); + await new TRRDNSListener("sub.top.test.com", { + expectedAnswer: "2.2.2.2", + }); + // blocklist expired. Do another check. + equal( + await trrServer.requestCount("sub.top.test.com", "A"), + 2, + "We should do another TRR request because the bloclist expired" + ); +}); diff --git a/netwerk/test/unit/test_trr_cancel.js b/netwerk/test/unit/test_trr_cancel.js new file mode 100644 index 0000000000..ce2ae52721 --- /dev/null +++ b/netwerk/test/unit/test_trr_cancel.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"; + +trr_test_setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); +}); + +let trrServer = null; +add_task(async function start_trr_server() { + trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + dump(`port = ${trrServer.port}\n`); + + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); +}); + +add_task(async function sanity_check() { + await trrServer.registerDoHAnswers("example.com", "A", { + answers: [ + { + name: "example.com", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + // Simple check to see that TRR works. + await new TRRDNSListener("example.com", { expectedAnswer: "1.2.3.4" }); +}); + +// Cancelling the request is not sync when using the socket process, so +// we skip this test when it's enabled. +add_task( + { skip_if: () => mozinfo.socketprocess_networking }, + async function cancel_immediately() { + await trrServer.registerDoHAnswers("example.org", "A", { + answers: [ + { + name: "example.org", + ttl: 55, + type: "A", + flush: false, + data: "2.3.4.5", + }, + ], + }); + let r1 = new TRRDNSListener("example.org", { expectedSuccess: false }); + let r2 = new TRRDNSListener("example.org", { expectedAnswer: "2.3.4.5" }); + r1.cancel(); + let { inStatus } = await r1; + equal(inStatus, Cr.NS_ERROR_ABORT); + await r2; + equal(await trrServer.requestCount("example.org", "A"), 1); + + // Now we cancel both of them + Services.dns.clearCache(true); + r1 = new TRRDNSListener("example.org", { expectedSuccess: false }); + r2 = new TRRDNSListener("example.org", { expectedSuccess: false }); + r1.cancel(); + r2.cancel(); + ({ inStatus } = await r1); + equal(inStatus, Cr.NS_ERROR_ABORT); + ({ inStatus } = await r2); + equal(inStatus, Cr.NS_ERROR_ABORT); + await new Promise(resolve => do_timeout(50, resolve)); + equal(await trrServer.requestCount("example.org", "A"), 2); + } +); + +add_task(async function cancel_delayed() { + Services.dns.clearCache(true); + await trrServer.registerDoHAnswers("example.com", "A", { + answers: [ + { + name: "example.com", + ttl: 55, + type: "A", + flush: false, + data: "1.1.1.1", + }, + ], + delay: 500, + }); + let r1 = new TRRDNSListener("example.com", { expectedSuccess: false }); + let r2 = new TRRDNSListener("example.com", { expectedAnswer: "1.1.1.1" }); + await new Promise(resolve => do_timeout(50, resolve)); + r1.cancel(); + let { inStatus } = await r1; + equal(inStatus, Cr.NS_ERROR_ABORT); + await r2; +}); + +add_task(async function cancel_after_completed() { + Services.dns.clearCache(true); + await trrServer.registerDoHAnswers("example.com", "A", { + answers: [ + { + name: "example.com", + ttl: 55, + type: "A", + flush: false, + data: "2.2.2.2", + }, + ], + }); + let r1 = new TRRDNSListener("example.com", { expectedAnswer: "2.2.2.2" }); + await r1; + let r2 = new TRRDNSListener("example.com", { expectedAnswer: "2.2.2.2" }); + // Check that cancelling r1 after it's complete does not affect r2 in any way. + r1.cancel(); + await r2; +}); + +add_task(async function clearCacheWhileResolving() { + Services.dns.clearCache(true); + await trrServer.registerDoHAnswers("example.com", "A", { + answers: [ + { + name: "example.com", + ttl: 55, + type: "A", + flush: false, + data: "3.3.3.3", + }, + ], + delay: 500, + }); + // Check that calling clearCache does not leave the request hanging. + let r1 = new TRRDNSListener("example.com", { expectedAnswer: "3.3.3.3" }); + let r2 = new TRRDNSListener("example.com", { expectedAnswer: "3.3.3.3" }); + Services.dns.clearCache(true); + await r1; + await r2; + + // Also check the same for HTTPS records + await trrServer.registerDoHAnswers("httpsvc.com", "HTTPS", { + answers: [ + { + name: "httpsvc.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.p1.com", + values: [{ key: "alpn", value: ["h2", "h3"] }], + }, + }, + ], + delay: 500, + }); + let r3 = new TRRDNSListener("httpsvc.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + let r4 = new TRRDNSListener("httpsvc.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + Services.dns.clearCache(true); + await r3; + await r4; + equal(await trrServer.requestCount("httpsvc.com", "HTTPS"), 1); + Services.dns.clearCache(true); + await new TRRDNSListener("httpsvc.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + equal(await trrServer.requestCount("httpsvc.com", "HTTPS"), 2); +}); diff --git a/netwerk/test/unit/test_trr_case_sensitivity.js b/netwerk/test/unit/test_trr_case_sensitivity.js new file mode 100644 index 0000000000..c2c9572fac --- /dev/null +++ b/netwerk/test/unit/test_trr_case_sensitivity.js @@ -0,0 +1,153 @@ +/* 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"; + +trr_test_setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish)); + }); +} + +add_task(async function test_trr_casing() { + let trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + dump(`port = ${trrServer.port}\n`); + let chan = makeChan(`https://localhost:${trrServer.port}/test?bla=some`); + let [, resp] = await channelOpenPromise(chan); + equal(resp, "<h1> 404 Path not found: /test?bla=some</h1>"); + + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + // This CNAME response goes to B.example.com (uppercased) + // It should be lowercased by the code + await trrServer.registerDoHAnswers("a.example.com", "A", { + answers: [ + { + name: "a.example.com", + ttl: 55, + type: "CNAME", + flush: false, + data: "B.example.com", + }, + ], + }); + // Like in bug 1635566, the response for B.example.com will be lowercased + // by the server too -> b.example.com + // Requesting this resource would case the browser to reject the resource + await trrServer.registerDoHAnswers("B.example.com", "A", { + answers: [ + { + name: "b.example.com", + ttl: 55, + type: "CNAME", + flush: false, + data: "c.example.com", + }, + ], + }); + + // The browser should request this one + await trrServer.registerDoHAnswers("b.example.com", "A", { + answers: [ + { + name: "b.example.com", + ttl: 55, + type: "CNAME", + flush: false, + data: "c.example.com", + }, + ], + }); + // Finally, it gets an IP + await trrServer.registerDoHAnswers("c.example.com", "A", { + answers: [ + { + name: "c.example.com", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + await new TRRDNSListener("a.example.com", { expectedAnswer: "1.2.3.4" }); + + await trrServer.registerDoHAnswers("a.test.com", "A", { + answers: [ + { + name: "a.test.com", + ttl: 55, + type: "CNAME", + flush: false, + data: "B.test.com", + }, + ], + }); + // We try this again, this time we explicitly make sure this resource + // is never used + await trrServer.registerDoHAnswers("B.test.com", "A", { + answers: [ + { + name: "B.test.com", + ttl: 55, + type: "A", + flush: false, + data: "9.9.9.9", + }, + ], + }); + await trrServer.registerDoHAnswers("b.test.com", "A", { + answers: [ + { + name: "b.test.com", + ttl: 55, + type: "A", + flush: false, + data: "8.8.8.8", + }, + ], + }); + await new TRRDNSListener("a.test.com", { expectedAnswer: "8.8.8.8" }); + + await trrServer.registerDoHAnswers("CAPITAL.COM", "A", { + answers: [ + { + name: "capital.com", + ttl: 55, + type: "A", + flush: false, + data: "2.2.2.2", + }, + ], + }); + await new TRRDNSListener("CAPITAL.COM", { expectedAnswer: "2.2.2.2" }); + await new TRRDNSListener("CAPITAL.COM.", { expectedAnswer: "2.2.2.2" }); + + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_trr_cname_chain.js b/netwerk/test/unit/test_trr_cname_chain.js new file mode 100644 index 0000000000..25bbbb3233 --- /dev/null +++ b/netwerk/test/unit/test_trr_cname_chain.js @@ -0,0 +1,231 @@ +/* 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"; + +let trrServer; + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish)); + }); +} + +add_setup(async function setup() { + trr_test_setup(); + registerCleanupFunction(async () => { + trr_clear_prefs(); + }); + + trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + dump(`port = ${trrServer.port}\n`); + let chan = makeChan(`https://localhost:${trrServer.port}/test?bla=some`); + let [, resp] = await channelOpenPromise(chan); + equal(resp, "<h1> 404 Path not found: /test?bla=some</h1>"); + + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); +}); + +add_task(async function test_follow_cnames_same_response() { + await trrServer.registerDoHAnswers("something.foo", "A", { + answers: [ + { + name: "something.foo", + ttl: 55, + type: "CNAME", + flush: false, + data: "other.foo", + }, + { + name: "other.foo", + ttl: 55, + type: "CNAME", + flush: false, + data: "bla.foo", + }, + { + name: "bla.foo", + ttl: 55, + type: "CNAME", + flush: false, + data: "xyz.foo", + }, + { + name: "xyz.foo", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + let { inRecord } = await new TRRDNSListener("something.foo", { + expectedAnswer: "1.2.3.4", + flags: Ci.nsIDNSService.RESOLVE_CANONICAL_NAME, + }); + equal(inRecord.QueryInterface(Ci.nsIDNSAddrRecord).canonicalName, "xyz.foo"); + + await trrServer.registerDoHAnswers("a.foo", "A", { + answers: [ + { + name: "a.foo", + ttl: 55, + type: "CNAME", + flush: false, + data: "b.foo", + }, + ], + }); + await trrServer.registerDoHAnswers("b.foo", "A", { + answers: [ + { + name: "b.foo", + ttl: 55, + type: "A", + flush: false, + data: "2.3.4.5", + }, + ], + }); + await new TRRDNSListener("a.foo", { expectedAnswer: "2.3.4.5" }); +}); + +add_task(async function test_cname_nodata() { + // Test that we don't needlessly follow cname chains when the RA flag is set + // on the response. + + await trrServer.registerDoHAnswers("first.foo", "A", { + flags: 0x80, + answers: [ + { + name: "first.foo", + ttl: 55, + type: "CNAME", + flush: false, + data: "second.foo", + }, + { + name: "second.foo", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + await trrServer.registerDoHAnswers("first.foo", "AAAA", { + flags: 0x80, + answers: [ + { + name: "first.foo", + ttl: 55, + type: "CNAME", + flush: false, + data: "second.foo", + }, + ], + }); + + await new TRRDNSListener("first.foo", { expectedAnswer: "1.2.3.4" }); + equal(await trrServer.requestCount("first.foo", "A"), 1); + equal(await trrServer.requestCount("first.foo", "AAAA"), 1); + equal(await trrServer.requestCount("second.foo", "A"), 0); + equal(await trrServer.requestCount("second.foo", "AAAA"), 0); + + await trrServer.registerDoHAnswers("first.bar", "A", { + answers: [ + { + name: "first.bar", + ttl: 55, + type: "CNAME", + flush: false, + data: "second.bar", + }, + { + name: "second.bar", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + await trrServer.registerDoHAnswers("first.bar", "AAAA", { + answers: [ + { + name: "first.bar", + ttl: 55, + type: "CNAME", + flush: false, + data: "second.bar", + }, + ], + }); + + await new TRRDNSListener("first.bar", { expectedAnswer: "1.2.3.4" }); + equal(await trrServer.requestCount("first.bar", "A"), 1); + equal(await trrServer.requestCount("first.bar", "AAAA"), 1); + equal(await trrServer.requestCount("second.bar", "A"), 0); // addr included in first response + equal(await trrServer.requestCount("second.bar", "AAAA"), 1); // will follow cname because no flag is set + + // Check that it also works for HTTPS records + + await trrServer.registerDoHAnswers("first.bar", "HTTPS", { + flags: 0x80, + answers: [ + { + name: "second.bar", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "h3pool", + values: [ + { key: "alpn", value: ["h2", "h3"] }, + { key: "no-default-alpn" }, + { key: "port", value: 8888 }, + { key: "ipv4hint", value: "1.2.3.4" }, + { key: "echconfig", value: "123..." }, + { key: "ipv6hint", value: "::1" }, + ], + }, + }, + { + name: "first.bar", + ttl: 55, + type: "CNAME", + flush: false, + data: "second.bar", + }, + ], + }); + + let { inStatus } = await new TRRDNSListener("first.bar", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + Assert.ok(Components.isSuccessCode(inStatus), `${inStatus} should work`); + equal(await trrServer.requestCount("first.bar", "HTTPS"), 1); + equal(await trrServer.requestCount("second.bar", "HTTPS"), 0); +}); diff --git a/netwerk/test/unit/test_trr_confirmation.js b/netwerk/test/unit/test_trr_confirmation.js new file mode 100644 index 0000000000..f7e50418b9 --- /dev/null +++ b/netwerk/test/unit/test_trr_confirmation.js @@ -0,0 +1,401 @@ +/* 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 { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +async function waitForConfirmationState(state, msToWait = 0) { + await TestUtils.waitForCondition( + () => Services.dns.currentTrrConfirmationState == state, + `Timed out waiting for ${state}. Currently ${Services.dns.currentTrrConfirmationState}`, + 1, + msToWait + ); + equal( + Services.dns.currentTrrConfirmationState, + state, + "expected confirmation state" + ); +} + +const CONFIRM_OFF = 0; +const CONFIRM_TRYING_OK = 1; +const CONFIRM_OK = 2; +const CONFIRM_FAILED = 3; +const CONFIRM_TRYING_FAILED = 4; +const CONFIRM_DISABLED = 5; + +function setup() { + trr_test_setup(); + Services.prefs.setBoolPref("network.trr.skip-check-for-blocked-host", true); +} + +setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.trr.skip-check-for-blocked-host"); +}); + +let trrServer = null; +add_task(async function start_trr_server() { + trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + dump(`port = ${trrServer.port}\n`); + + await trrServer.registerDoHAnswers(`faily.com`, "NS", { + answers: [ + { + name: "faily.com", + ttl: 55, + type: "NS", + flush: false, + data: "ns.faily.com", + }, + ], + }); + + for (let i = 0; i < 15; i++) { + await trrServer.registerDoHAnswers(`failing-domain${i}.faily.com`, "A", { + error: 600, + }); + await trrServer.registerDoHAnswers(`failing-domain${i}.faily.com`, "AAAA", { + error: 600, + }); + } +}); + +function trigger15Failures() { + // We need to clear the cache in case a previous call to this method + // put the results in the DNS cache. + Services.dns.clearCache(true); + + let dnsRequests = []; + // There are actually two TRR requests sent for A and AAAA records, so doing + // DNS query 10 times should be enough to trigger confirmation process. + for (let i = 0; i < 10; i++) { + dnsRequests.push( + new TRRDNSListener(`failing-domain${i}.faily.com`, { + expectedAnswer: "127.0.0.1", + }) + ); + } + + return Promise.all(dnsRequests); +} + +async function registerNS(delay) { + return trrServer.registerDoHAnswers("confirm.example.com", "NS", { + answers: [ + { + name: "confirm.example.com", + ttl: 55, + type: "NS", + flush: false, + data: "test.com", + }, + ], + delay, + }); +} + +add_task(async function confirm_off() { + Services.prefs.setCharPref( + "network.trr.confirmationNS", + "confirm.example.com" + ); + Services.prefs.setIntPref( + "network.trr.mode", + Ci.nsIDNSService.MODE_NATIVEONLY + ); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_OFF); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRROFF); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_OFF); +}); + +add_task(async function confirm_disabled() { + Services.prefs.setCharPref( + "network.trr.confirmationNS", + "confirm.example.com" + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_DISABLED); + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_DISABLED); +}); + +add_task(async function confirm_ok() { + Services.dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.confirmationNS", + "confirm.example.com" + ); + await registerNS(0); + await trrServer.registerDoHAnswers("example.com", "A", { + answers: [ + { + name: "example.com", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); + equal( + Services.dns.currentTrrConfirmationState, + CONFIRM_TRYING_OK, + "Should be CONFIRM_TRYING_OK" + ); + await new TRRDNSListener("example.com", { expectedAnswer: "1.2.3.4" }); + equal(await trrServer.requestCount("example.com", "A"), 1); + await waitForConfirmationState(CONFIRM_OK, 1000); + + await registerNS(500); + Services.prefs.setIntPref( + "network.trr.mode", + Ci.nsIDNSService.MODE_NATIVEONLY + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); + equal( + Services.dns.currentTrrConfirmationState, + CONFIRM_TRYING_OK, + "Should be CONFIRM_TRYING_OK" + ); + await new Promise(resolve => do_timeout(100, resolve)); + equal( + Services.dns.currentTrrConfirmationState, + CONFIRM_TRYING_OK, + "Confirmation should still be pending" + ); + await waitForConfirmationState(CONFIRM_OK, 1000); +}); + +add_task(async function confirm_timeout() { + Services.prefs.setIntPref( + "network.trr.mode", + Ci.nsIDNSService.MODE_NATIVEONLY + ); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_OFF); + await registerNS(7000); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); + equal( + Services.dns.currentTrrConfirmationState, + CONFIRM_TRYING_OK, + "Should be CONFIRM_TRYING_OK" + ); + await waitForConfirmationState(CONFIRM_FAILED, 7500); + // After the confirmation fails, a timer will periodically trigger a retry + // causing the state to go into CONFIRM_TRYING_FAILED. + await waitForConfirmationState(CONFIRM_TRYING_FAILED, 500); +}); + +add_task(async function confirm_fail_fast() { + Services.prefs.setIntPref( + "network.trr.mode", + Ci.nsIDNSService.MODE_NATIVEONLY + ); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_OFF); + await trrServer.registerDoHAnswers("confirm.example.com", "NS", { + error: 404, + }); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); + equal( + Services.dns.currentTrrConfirmationState, + CONFIRM_TRYING_OK, + "Should be CONFIRM_TRYING_OK" + ); + await waitForConfirmationState(CONFIRM_FAILED, 100); +}); + +add_task(async function multiple_failures() { + Services.prefs.setIntPref( + "network.trr.mode", + Ci.nsIDNSService.MODE_NATIVEONLY + ); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_OFF); + + await registerNS(100); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); + equal( + Services.dns.currentTrrConfirmationState, + CONFIRM_TRYING_OK, + "Should be CONFIRM_TRYING_OK" + ); + await waitForConfirmationState(CONFIRM_OK, 1000); + await registerNS(4000); + let failures = trigger15Failures(); + await waitForConfirmationState(CONFIRM_TRYING_OK, 3000); + await failures; + // Check that failures during confirmation are ignored. + await trigger15Failures(); + equal( + Services.dns.currentTrrConfirmationState, + CONFIRM_TRYING_OK, + "Should be CONFIRM_TRYING_OK" + ); + await waitForConfirmationState(CONFIRM_OK, 4500); +}); + +add_task(async function test_connectivity_change() { + await registerNS(100); + Services.prefs.setIntPref( + "network.trr.mode", + Ci.nsIDNSService.MODE_NATIVEONLY + ); + let confirmationCount = await trrServer.requestCount( + "confirm.example.com", + "NS" + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); + equal( + Services.dns.currentTrrConfirmationState, + CONFIRM_TRYING_OK, + "Should be CONFIRM_TRYING_OK" + ); + await waitForConfirmationState(CONFIRM_OK, 1000); + equal( + await trrServer.requestCount("confirm.example.com", "NS"), + confirmationCount + 1 + ); + Services.obs.notifyObservers( + null, + "network:captive-portal-connectivity", + "clear" + ); + // This means a CP check completed successfully. But no CP was previously + // detected, so this is mostly a no-op. + equal(Services.dns.currentTrrConfirmationState, CONFIRM_OK); + + Services.obs.notifyObservers( + null, + "network:captive-portal-connectivity", + "captive" + ); + // This basically a successful CP login event. Wasn't captive before. + // Still treating as a no-op. + equal(Services.dns.currentTrrConfirmationState, CONFIRM_OK); + + // This makes the TRR service set mCaptiveIsPassed=false + Services.obs.notifyObservers( + null, + "captive-portal-login", + "{type: 'captive-portal-login', id: 0, url: 'http://localhost/'}" + ); + + await registerNS(500); + let failures = trigger15Failures(); + // The failure should cause us to go into CONFIRM_TRYING_OK and do an NS req + await waitForConfirmationState(CONFIRM_TRYING_OK, 3000); + await failures; + + // The notification sets mCaptiveIsPassed=true then triggers an entirely new + // confirmation. + Services.obs.notifyObservers( + null, + "network:captive-portal-connectivity", + "clear" + ); + // The notification should cause us to send a new confirmation request + equal( + Services.dns.currentTrrConfirmationState, + CONFIRM_TRYING_OK, + "Should be CONFIRM_TRYING_OK" + ); + await waitForConfirmationState(CONFIRM_OK, 1000); + // two extra confirmation events should have been received by the server + equal( + await trrServer.requestCount("confirm.example.com", "NS"), + confirmationCount + 3 + ); +}); + +add_task(async function test_network_change() { + let confirmationCount = await trrServer.requestCount( + "confirm.example.com", + "NS" + ); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_OK); + + Services.obs.notifyObservers(null, "network:link-status-changed", "up"); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_OK); + equal( + await trrServer.requestCount("confirm.example.com", "NS"), + confirmationCount + ); + + let failures = trigger15Failures(); + // The failure should cause us to go into CONFIRM_TRYING_OK and do an NS req + await waitForConfirmationState(CONFIRM_TRYING_OK, 3000); + await failures; + // The network up event should reset the confirmation to TRYING_OK and do + // another NS req + Services.obs.notifyObservers(null, "network:link-status-changed", "up"); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_TRYING_OK); + await waitForConfirmationState(CONFIRM_OK, 1000); + // two extra confirmation events should have been received by the server + equal( + await trrServer.requestCount("confirm.example.com", "NS"), + confirmationCount + 2 + ); +}); + +add_task(async function test_uri_pref_change() { + let confirmationCount = await trrServer.requestCount( + "confirm.example.com", + "NS" + ); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_OK); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query?changed` + ); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_TRYING_OK); + await waitForConfirmationState(CONFIRM_OK, 1000); + equal( + await trrServer.requestCount("confirm.example.com", "NS"), + confirmationCount + 1 + ); +}); + +add_task(async function test_autodetected_uri() { + const defaultPrefBranch = Services.prefs.getDefaultBranch(""); + let defaultURI = defaultPrefBranch.getCharPref( + "network.trr.default_provider_uri" + ); + defaultPrefBranch.setCharPref( + "network.trr.default_provider_uri", + `https://foo.example.com:${trrServer.port}/dns-query?changed` + ); + // For setDetectedTrrURI to work we must pretend we are using the default. + Services.prefs.clearUserPref("network.trr.uri"); + await waitForConfirmationState(CONFIRM_OK, 1000); + let confirmationCount = await trrServer.requestCount( + "confirm.example.com", + "NS" + ); + Services.dns.setDetectedTrrURI( + `https://foo.example.com:${trrServer.port}/dns-query?changed2` + ); + equal(Services.dns.currentTrrConfirmationState, CONFIRM_TRYING_OK); + await waitForConfirmationState(CONFIRM_OK, 1000); + equal( + await trrServer.requestCount("confirm.example.com", "NS"), + confirmationCount + 1 + ); + + // reset the default URI + defaultPrefBranch.setCharPref("network.trr.default_provider_uri", defaultURI); +}); diff --git a/netwerk/test/unit/test_trr_decoding.js b/netwerk/test/unit/test_trr_decoding.js new file mode 100644 index 0000000000..7f7c2639aa --- /dev/null +++ b/netwerk/test/unit/test_trr_decoding.js @@ -0,0 +1,56 @@ +/* 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"; + +trr_test_setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); +}); + +let trrServer = null; +add_setup(async function start_trr_server() { + trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + dump(`port = ${trrServer.port}\n`); + + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY); +}); + +add_task(async function ignoreUnknownTypes() { + Services.dns.clearCache(true); + await trrServer.registerDoHAnswers("abc.def.ced.com", "A", { + answers: [ + { + name: "abc.def.ced.com", + ttl: 55, + type: "DNAME", + flush: false, + data: "def.ced.com.test", + }, + { + name: "abc.def.ced.com", + ttl: 55, + type: "CNAME", + flush: false, + data: "abc.def.ced.com.test", + }, + { + name: "abc.def.ced.com.test", + ttl: 55, + type: "A", + flush: false, + data: "3.3.3.3", + }, + ], + }); + await new TRRDNSListener("abc.def.ced.com", { expectedAnswer: "3.3.3.3" }); +}); diff --git a/netwerk/test/unit/test_trr_domain.js b/netwerk/test/unit/test_trr_domain.js new file mode 100644 index 0000000000..f154060706 --- /dev/null +++ b/netwerk/test/unit/test_trr_domain.js @@ -0,0 +1,123 @@ +/* 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"; + +// This test simmulates intermittent native DNS functionality. +// We verify that we don't use the negative DNS record for the DoH server. +// The first resolve of foo.example.com fails, so we expect TRR not to work. +// Immediately after the native DNS starts working, it should connect to the +// TRR server and start working. + +const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); + +function setup() { + trr_test_setup(); + Services.prefs.clearUserPref("network.trr.bootstrapAddr"); + Services.prefs.clearUserPref("network.dns.native-is-localhost"); +} +setup(); + +registerCleanupFunction(async () => { + trr_clear_prefs(); + override.clearOverrides(); +}); + +add_task(async function intermittent_dns_mode3() { + override.addIPOverride("foo.example.com", "N/A"); + let trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + info(`port = ${trrServer.port}\n`); + Services.dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY); + await trrServer.registerDoHAnswers("example.com", "A", { + answers: [ + { + name: "example.com", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + let { inStatus } = await new TRRDNSListener("example.com", { + expectedSuccess: false, + }); + equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); + await trrServer.registerDoHAnswers("example.org", "A", { + answers: [ + { + name: "example.org", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + override.addIPOverride("foo.example.com", "127.0.0.1"); + await new TRRDNSListener("example.org", { expectedAnswer: "1.2.3.4" }); + await trrServer.stop(); +}); + +add_task(async function intermittent_dns_mode2() { + override.addIPOverride("foo.example.com", "N/A"); + let trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + info(`port = ${trrServer.port}\n`); + + Services.dns.clearCache(true); + Services.prefs.setIntPref( + "network.trr.mode", + Ci.nsIDNSService.MODE_NATIVEONLY + ); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); + await trrServer.registerDoHAnswers("example.com", "A", { + answers: [ + { + name: "example.com", + ttl: 55, + type: "A", + flush: false, + data: "1.1.1.1", + }, + ], + }); + override.addIPOverride("example.com", "2.2.2.2"); + await new TRRDNSListener("example.com", { + expectedAnswer: "2.2.2.2", + }); + await trrServer.registerDoHAnswers("example.org", "A", { + answers: [ + { + name: "example.org", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + override.addIPOverride("example.org", "3.3.3.3"); + override.addIPOverride("foo.example.com", "127.0.0.1"); + await new TRRDNSListener("example.org", { expectedAnswer: "1.2.3.4" }); + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_trr_enterprise_policy.js b/netwerk/test/unit/test_trr_enterprise_policy.js new file mode 100644 index 0000000000..e96753d554 --- /dev/null +++ b/netwerk/test/unit/test_trr_enterprise_policy.js @@ -0,0 +1,93 @@ +/* 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"; + +trr_test_setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); +}); + +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); + +updateAppInfo({ + name: "XPCShell", + ID: "xpcshell@tests.mozilla.org", + version: "48", + platformVersion: "48", +}); + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +// This initializes the policy engine for xpcshell tests +let policies = Cc["@mozilla.org/enterprisepolicies;1"].getService( + Ci.nsIObserver +); +policies.observe(null, "policies-startup", null); + +add_task(async function test_enterprise_policy_unlocked() { + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + DNSOverHTTPS: { + Enabled: false, + ProviderURL: "https://example.org/provider", + ExcludedDomains: ["example.com", "example.org"], + }, + }, + }); + + equal(Services.prefs.getIntPref("network.trr.mode"), 5); + equal(Services.prefs.prefIsLocked("network.trr.mode"), false); + equal( + Services.prefs.getStringPref("network.trr.uri"), + "https://example.org/provider" + ); + equal(Services.prefs.prefIsLocked("network.trr.uri"), false); + equal( + Services.prefs.getStringPref("network.trr.excluded-domains"), + "example.com,example.org" + ); + equal(Services.prefs.prefIsLocked("network.trr.excluded-domains"), false); + equal(Services.dns.currentTrrMode, 5); + equal(Services.dns.currentTrrURI, "https://example.org/provider"); + Services.dns.setDetectedTrrURI("https://autodetect.example.com/provider"); + equal(Services.dns.currentTrrMode, 5); + equal(Services.dns.currentTrrURI, "https://example.org/provider"); +}); + +add_task(async function test_enterprise_policy_locked() { + // Read dns.currentTrrMode to make DNS service initialized earlier. + info("Services.dns.currentTrrMode:" + Services.dns.currentTrrMode); + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + DNSOverHTTPS: { + Enabled: true, + ProviderURL: "https://example.com/provider", + ExcludedDomains: ["example.com", "example.org"], + Locked: true, + }, + }, + }); + + equal(Services.prefs.getIntPref("network.trr.mode"), 2); + equal(Services.prefs.prefIsLocked("network.trr.mode"), true); + equal( + Services.prefs.getStringPref("network.trr.uri"), + "https://example.com/provider" + ); + equal(Services.prefs.prefIsLocked("network.trr.uri"), true); + equal( + Services.prefs.getStringPref("network.trr.excluded-domains"), + "example.com,example.org" + ); + equal(Services.prefs.prefIsLocked("network.trr.excluded-domains"), true); + equal(Services.dns.currentTrrMode, 2); + equal(Services.dns.currentTrrURI, "https://example.com/provider"); + Services.dns.setDetectedTrrURI("https://autodetect.example.com/provider"); + equal(Services.dns.currentTrrURI, "https://example.com/provider"); +}); diff --git a/netwerk/test/unit/test_trr_extended_error.js b/netwerk/test/unit/test_trr_extended_error.js new file mode 100644 index 0000000000..13f1b8a8df --- /dev/null +++ b/netwerk/test/unit/test_trr_extended_error.js @@ -0,0 +1,319 @@ +/* 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"; + +trr_test_setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish)); + }); +} + +let trrServer; +add_task(async function setup() { + trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + dump(`port = ${trrServer.port}\n`); + let chan = makeChan(`https://localhost:${trrServer.port}/test?bla=some`); + let [, resp] = await channelOpenPromise(chan); + equal(resp, "<h1> 404 Path not found: /test?bla=some</h1>"); + + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); +}); + +add_task(async function test_extended_error_bogus() { + await trrServer.registerDoHAnswers("something.foo", "A", { + answers: [ + { + name: "something.foo", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + + await new TRRDNSListener("something.foo", { expectedAnswer: "1.2.3.4" }); + + await trrServer.registerDoHAnswers("a.foo", "A", { + answers: [], + additionals: [ + { + name: ".", + type: "OPT", + class: "IN", + options: [ + { + code: "EDNS_ERROR", + extended_error: 6, // DNSSEC_BOGUS + text: "DNSSec bogus", + }, + ], + }, + ], + flags: 2, // SERVFAIL + }); + + // Check that we don't fall back to DNS + let { inStatus } = await new TRRDNSListener("a.foo", { + expectedSuccess: false, + }); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); +}); + +add_task(async function test_extended_error_filtered() { + await trrServer.registerDoHAnswers("b.foo", "A", { + answers: [], + additionals: [ + { + name: ".", + type: "OPT", + class: "IN", + options: [ + { + code: "EDNS_ERROR", + extended_error: 17, // Filtered + text: "Filtered", + }, + ], + }, + ], + }); + + // Check that we don't fall back to DNS + let { inStatus } = await new TRRDNSListener("b.foo", { + expectedSuccess: false, + }); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); +}); + +add_task(async function test_extended_error_not_ready() { + await trrServer.registerDoHAnswers("c.foo", "A", { + answers: [], + additionals: [ + { + name: ".", + type: "OPT", + class: "IN", + options: [ + { + code: "EDNS_ERROR", + extended_error: 14, // Not ready + text: "Not ready", + }, + ], + }, + ], + }); + + // For this code it's OK to fallback + await new TRRDNSListener("c.foo", { expectedAnswer: "127.0.0.1" }); +}); + +add_task(async function ipv6_answer_and_delayed_ipv4_error() { + // AAAA comes back immediately. + // A EDNS_ERROR comes back later, with a delay + await trrServer.registerDoHAnswers("delay1.com", "AAAA", { + answers: [ + { + name: "delay1.com", + ttl: 55, + type: "AAAA", + flush: false, + data: "::a:b:c:d", + }, + ], + }); + await trrServer.registerDoHAnswers("delay1.com", "A", { + answers: [], + additionals: [ + { + name: ".", + type: "OPT", + class: "IN", + options: [ + { + code: "EDNS_ERROR", + extended_error: 17, // Filtered + text: "Filtered", + }, + ], + }, + ], + delay: 200, + }); + + // Check that we don't fall back to DNS + await new TRRDNSListener("delay1.com", { expectedAnswer: "::a:b:c:d" }); +}); + +add_task(async function ipv4_error_and_delayed_ipv6_answer() { + // AAAA comes back immediately delay + // A EDNS_ERROR comes back immediately + await trrServer.registerDoHAnswers("delay2.com", "AAAA", { + answers: [ + { + name: "delay2.com", + ttl: 55, + type: "AAAA", + flush: false, + data: "::a:b:c:d", + }, + ], + delay: 200, + }); + await trrServer.registerDoHAnswers("delay2.com", "A", { + answers: [], + additionals: [ + { + name: ".", + type: "OPT", + class: "IN", + options: [ + { + code: "EDNS_ERROR", + extended_error: 17, // Filtered + text: "Filtered", + }, + ], + }, + ], + }); + + // Check that we don't fall back to DNS + await new TRRDNSListener("delay2.com", { expectedAnswer: "::a:b:c:d" }); +}); + +add_task(async function ipv4_answer_and_delayed_ipv6_error() { + // A comes back immediately. + // AAAA EDNS_ERROR comes back later, with a delay + await trrServer.registerDoHAnswers("delay3.com", "A", { + answers: [ + { + name: "delay3.com", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + await trrServer.registerDoHAnswers("delay3.com", "AAAA", { + answers: [], + additionals: [ + { + name: ".", + type: "OPT", + class: "IN", + options: [ + { + code: "EDNS_ERROR", + extended_error: 17, // Filtered + text: "Filtered", + }, + ], + }, + ], + delay: 200, + }); + + // Check that we don't fall back to DNS + await new TRRDNSListener("delay3.com", { expectedAnswer: "1.2.3.4" }); +}); + +add_task(async function delayed_ipv4_answer_and_ipv6_error() { + // A comes back with delay. + // AAAA EDNS_ERROR comes immediately + await trrServer.registerDoHAnswers("delay4.com", "A", { + answers: [ + { + name: "delay4.com", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + delay: 200, + }); + await trrServer.registerDoHAnswers("delay4.com", "AAAA", { + answers: [], + additionals: [ + { + name: ".", + type: "OPT", + class: "IN", + options: [ + { + code: "EDNS_ERROR", + extended_error: 17, // Filtered + text: "Filtered", + }, + ], + }, + ], + }); + + // Check that we don't fall back to DNS + await new TRRDNSListener("delay4.com", { expectedAnswer: "1.2.3.4" }); +}); + +add_task(async function test_only_ipv4_extended_error() { + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + await trrServer.registerDoHAnswers("only.com", "A", { + answers: [], + additionals: [ + { + name: ".", + type: "OPT", + class: "IN", + options: [ + { + code: "EDNS_ERROR", + extended_error: 17, // Filtered + text: "Filtered", + }, + ], + }, + ], + }); + let { inStatus } = await new TRRDNSListener("only.com", { + expectedSuccess: false, + }); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); +}); diff --git a/netwerk/test/unit/test_trr_https_fallback.js b/netwerk/test/unit/test_trr_https_fallback.js new file mode 100644 index 0000000000..1b5217876d --- /dev/null +++ b/netwerk/test/unit/test_trr_https_fallback.js @@ -0,0 +1,1105 @@ +/* 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 { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +let h2Port; +let h3Port; +let h3NoResponsePort; +let trrServer; + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +add_setup(async function setup() { + trr_test_setup(); + + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + h3NoResponsePort = Services.env.get("MOZHTTP3_PORT_NO_RESPONSE"); + Assert.notEqual(h3NoResponsePort, null); + Assert.notEqual(h3NoResponsePort, ""); + + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + + registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + Services.prefs.clearUserPref("network.dns.echconfig.enabled"); + Services.prefs.clearUserPref( + "network.dns.echconfig.fallback_to_origin_when_all_failed" + ); + Services.prefs.clearUserPref("network.dns.httpssvc.reset_exclustion_list"); + Services.prefs.clearUserPref("network.http.http3.enable"); + Services.prefs.clearUserPref( + "network.dns.httpssvc.http3_fast_fallback_timeout" + ); + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled"); + if (trrServer) { + await trrServer.stop(); + } + }); + + if (mozinfo.socketprocess_networking) { + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } + + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + } + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +// Test if we can fallback to the last record sucessfully. +add_task(async function testFallbackToTheLastRecord() { + trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + // Only the last record is valid to use. + await trrServer.registerDoHAnswers("test.fallback.com", "HTTPS", { + answers: [ + { + name: "test.fallback.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.fallback1.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "echconfig", value: "123..." }, + ], + }, + }, + { + name: "test.fallback.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 4, + name: "foo.example.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.fallback.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 3, + name: "test.fallback3.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.fallback.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.fallback2.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await new TRRDNSListener("test.fallback.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let chan = makeChan(`https://test.fallback.com:${h2Port}/server-timing`); + let [req] = await channelOpenPromise(chan); + // Test if this request is done by h2. + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + await trrServer.stop(); +}); + +add_task(async function testFallbackToTheOrigin() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setBoolPref( + "network.dns.echconfig.fallback_to_origin_when_all_failed", + true + ); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + // All records are not able to use to connect, so we fallback to the origin + // one. + await trrServer.registerDoHAnswers("test.foo.com", "HTTPS", { + answers: [ + { + name: "test.foo.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.foo1.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "echconfig", value: "123..." }, + ], + }, + }, + { + name: "test.foo.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 3, + name: "test.foo3.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.foo.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.foo2.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("test.foo.com", "A", { + answers: [ + { + name: "test.foo.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await new TRRDNSListener("test.foo.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let chan = makeChan(`https://test.foo.com:${h2Port}/server-timing`); + let [req] = await channelOpenPromise(chan); + // Test if this request is done by h2. + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + await trrServer.stop(); +}); + +// Test when all records are failed and network.dns.echconfig.fallback_to_origin +// is false. In this case, the connection is always failed. +add_task(async function testAllRecordsFailed() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref( + "network.dns.echconfig.fallback_to_origin_when_all_failed", + false + ); + + await trrServer.registerDoHAnswers("test.bar.com", "HTTPS", { + answers: [ + { + name: "test.bar.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.bar1.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "echconfig", value: "123..." }, + ], + }, + }, + { + name: "test.bar.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 3, + name: "test.bar3.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.bar.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.bar2.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await new TRRDNSListener("test.bar.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + // This channel should be failed. + let chan = makeChan(`https://test.bar.com:${h2Port}/server-timing`); + await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL); + + await trrServer.stop(); +}); + +// Test when all records have no echConfig, we directly fallback to the origin +// one. +add_task(async function testFallbackToTheOrigin2() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.example.com", "HTTPS", { + answers: [ + { + name: "test.example.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.example1.com", + values: [{ key: "alpn", value: ["h2", "h3-26"] }], + }, + }, + { + name: "test.example.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 3, + name: "test.example3.com", + values: [{ key: "alpn", value: ["h2", "h3-26"] }], + }, + }, + ], + }); + + await new TRRDNSListener("test.example.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let chan = makeChan(`https://test.example.com:${h2Port}/server-timing`); + await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL); + + await trrServer.registerDoHAnswers("test.example.com", "A", { + answers: [ + { + name: "test.example.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + chan = makeChan(`https://test.example.com:${h2Port}/server-timing`); + await channelOpenPromise(chan); + + await trrServer.stop(); +}); + +// Test when some records have echConfig and some not, we directly fallback to +// the origin one. +add_task(async function testFallbackToTheOrigin3() { + Services.dns.clearCache(true); + + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("vulnerable.com", "A", { + answers: [ + { + name: "vulnerable.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await trrServer.registerDoHAnswers("vulnerable.com", "HTTPS", { + answers: [ + { + name: "vulnerable.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "vulnerable1.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "vulnerable.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "vulnerable2.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "vulnerable.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 3, + name: "vulnerable3.com", + values: [{ key: "alpn", value: ["h2", "h3-26"] }], + }, + }, + ], + }); + + await new TRRDNSListener("vulnerable.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let chan = makeChan(`https://vulnerable.com:${h2Port}/server-timing`); + await channelOpenPromise(chan); + + await trrServer.stop(); +}); + +add_task(async function testResetExclusionList() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref( + "network.dns.httpssvc.reset_exclustion_list", + false + ); + + await trrServer.registerDoHAnswers("test.reset.com", "HTTPS", { + answers: [ + { + name: "test.reset.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.reset1.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.reset.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.reset2.com", + values: [ + { key: "alpn", value: ["h2", "h3-26"] }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await new TRRDNSListener("test.reset.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + // After this request, test.reset1.com and test.reset2.com should be both in + // the exclusion list. + let chan = makeChan(`https://test.reset.com:${h2Port}/server-timing`); + await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL); + + // This request should be also failed, because all records are excluded. + chan = makeChan(`https://test.reset.com:${h2Port}/server-timing`); + await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL); + + await trrServer.registerDoHAnswers("test.reset1.com", "A", { + answers: [ + { + name: "test.reset1.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + Services.prefs.setBoolPref( + "network.dns.httpssvc.reset_exclustion_list", + true + ); + + // After enable network.dns.httpssvc.reset_exclustion_list and register + // A record for test.reset1.com, this request should be succeeded. + chan = makeChan(`https://test.reset.com:${h2Port}/server-timing`); + await channelOpenPromise(chan); + + await trrServer.stop(); +}); + +// Simply test if we can connect to H3 server. +add_task(async function testH3Connection() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 100 + ); + + await trrServer.registerDoHAnswers("test.h3.com", "HTTPS", { + answers: [ + { + name: "test.h3.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "www.h3.com", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("www.h3.com", "A", { + answers: [ + { + name: "www.h3.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await new TRRDNSListener("test.h3.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let chan = makeChan(`https://test.h3.com`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h3-29"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h3Port); + + await trrServer.stop(); +}); + +add_task(async function testFastfallbackToH2() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + // Use a short timeout to make sure the fast fallback timer will be triggered. + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 1 + ); + Services.prefs.setCharPref( + "network.dns.localDomains", + "test.fastfallback1.com" + ); + + await trrServer.registerDoHAnswers("test.fastfallback.com", "HTTPS", { + answers: [ + { + name: "test.fastfallback.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.fastfallback1.com", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3NoResponsePort }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.fastfallback.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.fastfallback2.com", + values: [ + { key: "alpn", value: "h2" }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("test.fastfallback2.com", "A", { + answers: [ + { + name: "test.fastfallback2.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await new TRRDNSListener("test.fastfallback.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let chan = makeChan(`https://test.fastfallback.com/server-timing`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + + // Use a longer timeout to test the case that the timer is canceled. + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 5000 + ); + + chan = makeChan(`https://test.fastfallback.com/server-timing`); + [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + + await trrServer.stop(); +}); + +// Test when we fail to establish H3 connection. +add_task(async function testFailedH3Connection() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 0 + ); + + await trrServer.registerDoHAnswers("test.h3.org", "HTTPS", { + answers: [ + { + name: "test.h3.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "www.h3.org", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await new TRRDNSListener("test.h3.org", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let chan = makeChan(`https://test.h3.org`); + await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL); + + await trrServer.stop(); +}); + +// Test we don't use the service mode record whose domain is in +// http3 excluded list. +add_task(async function testHttp3ExcludedList() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 0 + ); + + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "www.h3_fail.org;h3-29=:" + h3Port + ); + + // This will fail because there is no address record for www.h3_fail.org. + let chan = makeChan(`https://www.h3_fail.org`); + await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL); + + // Now www.h3_fail.org should be already excluded, so the second record + // foo.example.com will be selected. + await trrServer.registerDoHAnswers("test.h3_excluded.org", "HTTPS", { + answers: [ + { + name: "test.h3_excluded.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "www.h3_fail.org", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + ], + }, + }, + { + name: "test.h3_excluded.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "foo.example.com", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + ], + }, + }, + ], + }); + + await new TRRDNSListener("test.h3_excluded.org", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + chan = makeChan(`https://test.h3_excluded.org`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h3-29"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h3Port); + + await trrServer.stop(); +}); + +add_task(async function testAllRecordsInHttp3ExcludedList() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 0 + ); + + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "www.h3_fail1.org;h3-29=:" + h3Port + ); + + await trrServer.registerDoHAnswers("www.h3_all_excluded.org", "A", { + answers: [ + { + name: "www.h3_all_excluded.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + // Test we can connect to www.h3_all_excluded.org sucessfully. + let chan = makeChan( + `https://www.h3_all_excluded.org:${h2Port}/server-timing` + ); + + let [req] = await channelOpenPromise(chan); + + // Test if this request is done by h2. + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + // This will fail because there is no address record for www.h3_fail1.org. + chan = makeChan(`https://www.h3_fail1.org`); + await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL); + + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "www.h3_fail2.org;h3-29=:" + h3Port + ); + + // This will fail because there is no address record for www.h3_fail2.org. + chan = makeChan(`https://www.h3_fail2.org`); + await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL); + + await trrServer.registerDoHAnswers("www.h3_all_excluded.org", "HTTPS", { + answers: [ + { + name: "www.h3_all_excluded.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "www.h3_fail1.org", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "www.h3_all_excluded.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "www.h3_fail2.org", + values: [ + { key: "alpn", value: "h3-29" }, + { key: "port", value: h3Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await new TRRDNSListener("www.h3_all_excluded.org", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + Services.obs.notifyObservers(null, "net:prune-all-connections"); + + // All HTTPS RRs are in http3 excluded list and all records are failed to + // connect, so don't fallback to the origin one. + chan = makeChan(`https://www.h3_all_excluded.org:${h2Port}/server-timing`); + await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL); + + await trrServer.registerDoHAnswers("www.h3_fail1.org", "A", { + answers: [ + { + name: "www.h3_fail1.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + // The the case that when all records are in http3 excluded list, we still + // give the first record one more shot. + chan = makeChan(`https://www.h3_all_excluded.org`); + [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h3-29"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h3Port); + + await trrServer.stop(); +}); + +WebSocketListener.prototype = { + onAcknowledge(aContext, aSize) {}, + onBinaryMessageAvailable(aContext, aMsg) {}, + onMessageAvailable(aContext, aMsg) {}, + onServerClose(aContext, aCode, aReason) {}, + onStart(aContext) { + this.finish(); + }, + onStop(aContext, aStatusCode) {}, +}; + +add_task(async function testUpgradeNotUsingHTTPSRR() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.ws.com", "HTTPS", { + answers: [ + { + name: "test.ws.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.ws1.com", + values: [{ key: "port", value: ["8888"] }], + }, + }, + ], + }); + + await new TRRDNSListener("test.ws.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + await trrServer.registerDoHAnswers("test.ws.com", "A", { + answers: [ + { + name: "test.ws.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + let wssUri = "wss://test.ws.com:" + h2Port + "/websocket"; + let chan = Cc["@mozilla.org/network/protocol;1?name=wss"].createInstance( + Ci.nsIWebSocketChannel + ); + chan.initLoadInfo( + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_DOCUMENT + ); + + var uri = Services.io.newURI(wssUri); + var wsListener = new WebSocketListener(); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + await new Promise(resolve => { + wsListener.finish = resolve; + chan.asyncOpen(uri, wssUri, {}, 0, wsListener, null); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + }); + + await trrServer.stop(); +}); + +// Test if we fallback to h2 with echConfig. +add_task(async function testFallbackToH2WithEchConfig() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 0 + ); + + await trrServer.registerDoHAnswers("test.fallback.org", "HTTPS", { + answers: [ + { + name: "test.fallback.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.fallback.org", + values: [ + { key: "alpn", value: ["h2", "h3-29"] }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ], + }); + + await trrServer.registerDoHAnswers("test.fallback.org", "A", { + answers: [ + { + name: "test.fallback.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await new TRRDNSListener("test.fallback.org", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + await new TRRDNSListener("test.fallback.org", "127.0.0.1"); + + let chan = makeChan(`https://test.fallback.org/server-timing`); + let [req] = await channelOpenPromise(chan); + // Test if this request is done by h2. + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_trr_httpssvc.js b/netwerk/test/unit/test_trr_httpssvc.js new file mode 100644 index 0000000000..cf57726ab6 --- /dev/null +++ b/netwerk/test/unit/test_trr_httpssvc.js @@ -0,0 +1,728 @@ +/* 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 { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +let h2Port; +let trrServer; + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +add_setup(async function setup() { + if (inChildProcess()) { + return; + } + + trr_test_setup(); + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.dns.port_prefixed_qname_https_rr"); + await trrServer.stop(); + }); + + if (mozinfo.socketprocess_networking) { + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } + + Services.prefs.setIntPref("network.trr.mode", 3); +}); + +add_task(async function testHTTPSSVC() { + // use the h2 server as DOH provider + if (!inChildProcess()) { + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/httpssvc" + ); + } + + let { inRecord } = await new TRRDNSListener("test.httpssvc.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer[0].priority, 1); + Assert.equal(answer[0].name, "h3pool"); + Assert.equal(answer[0].values.length, 7); + Assert.deepEqual( + answer[0].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn, + ["h2", "h3"], + "got correct answer" + ); + Assert.ok( + answer[0].values[1].QueryInterface(Ci.nsISVCParamNoDefaultAlpn), + "got correct answer" + ); + Assert.equal( + answer[0].values[2].QueryInterface(Ci.nsISVCParamPort).port, + 8888, + "got correct answer" + ); + Assert.equal( + answer[0].values[3].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0] + .address, + "1.2.3.4", + "got correct answer" + ); + Assert.equal( + answer[0].values[4].QueryInterface(Ci.nsISVCParamEchConfig).echconfig, + "123...", + "got correct answer" + ); + Assert.equal( + answer[0].values[5].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0] + .address, + "::1", + "got correct answer" + ); + Assert.equal( + answer[0].values[6].QueryInterface(Ci.nsISVCParamODoHConfig).ODoHConfig, + "456...", + "got correct answer" + ); + Assert.equal(answer[1].priority, 2); + Assert.equal(answer[1].name, "test.httpssvc.com"); + Assert.equal(answer[1].values.length, 5); + Assert.deepEqual( + answer[1].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn, + ["h2"], + "got correct answer" + ); + Assert.equal( + answer[1].values[1].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0] + .address, + "1.2.3.4", + "got correct answer" + ); + Assert.equal( + answer[1].values[1].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[1] + .address, + "5.6.7.8", + "got correct answer" + ); + Assert.equal( + answer[1].values[2].QueryInterface(Ci.nsISVCParamEchConfig).echconfig, + "abc...", + "got correct answer" + ); + Assert.equal( + answer[1].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0] + .address, + "::1", + "got correct answer" + ); + Assert.equal( + answer[1].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[1] + .address, + "fe80::794f:6d2c:3d5e:7836", + "got correct answer" + ); + Assert.equal( + answer[1].values[4].QueryInterface(Ci.nsISVCParamODoHConfig).ODoHConfig, + "def...", + "got correct answer" + ); + Assert.equal(answer[2].priority, 3); + Assert.equal(answer[2].name, "hello"); + Assert.equal(answer[2].values.length, 0); +}); + +add_task(async function test_aliasform() { + trrServer = new TRRServer(); + await trrServer.start(); + dump(`port = ${trrServer.port}\n`); + + if (inChildProcess()) { + do_send_remote_message("mode3-port", trrServer.port); + await do_await_remote_message("mode3-port-done"); + } else { + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + } + + // Make sure that HTTPS AliasForm is only treated as a CNAME for HTTPS requests + await trrServer.registerDoHAnswers("test1.com", "A", { + answers: [ + { + name: "test1.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 0, + name: "something1.com", + values: [], + }, + }, + ], + }); + await trrServer.registerDoHAnswers("something1.com", "A", { + answers: [ + { + name: "something1.com", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + + { + let { inStatus } = await new TRRDNSListener("test1.com", { + expectedSuccess: false, + }); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + } + + // Test that HTTPS priority = 0 (AliasForm) behaves like a CNAME + await trrServer.registerDoHAnswers("test.com", "HTTPS", { + answers: [ + { + name: "test.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 0, + name: "something.com", + values: [], + }, + }, + ], + }); + await trrServer.registerDoHAnswers("something.com", "HTTPS", { + answers: [ + { + name: "something.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "h3pool", + values: [{ key: "alpn", value: ["h2", "h3"] }], + }, + }, + ], + }); + + { + let { inStatus, inRecord } = await new TRRDNSListener("test.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + expectedSuccess: false, + }); + Assert.ok(Components.isSuccessCode(inStatus), `${inStatus} should succeed`); + let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer[0].priority, 1); + Assert.equal(answer[0].name, "h3pool"); + } + + // Test a chain of HTTPSSVC AliasForm and CNAMEs + await trrServer.registerDoHAnswers("x.com", "HTTPS", { + answers: [ + { + name: "x.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 0, + name: "y.com", + values: [], + }, + }, + ], + }); + await trrServer.registerDoHAnswers("y.com", "HTTPS", { + answers: [ + { + name: "y.com", + type: "CNAME", + ttl: 55, + class: "IN", + flush: false, + data: "z.com", + }, + ], + }); + await trrServer.registerDoHAnswers("z.com", "HTTPS", { + answers: [ + { + name: "z.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 0, + name: "target.com", + values: [], + }, + }, + ], + }); + await trrServer.registerDoHAnswers("target.com", "HTTPS", { + answers: [ + { + name: "target.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "h3pool", + values: [{ key: "alpn", value: ["h2", "h3"] }], + }, + }, + ], + }); + + let { inStatus, inRecord } = await new TRRDNSListener("x.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + expectedSuccess: false, + }); + Assert.ok(Components.isSuccessCode(inStatus), `${inStatus} should succeed`); + let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer[0].priority, 1); + Assert.equal(answer[0].name, "h3pool"); + + // We get a ServiceForm instead of a A answer, CNAME or AliasForm + await trrServer.registerDoHAnswers("no-ip-host.com", "A", { + answers: [ + { + name: "no-ip-host.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "h3pool", + values: [ + { key: "alpn", value: ["h2", "h3"] }, + { key: "no-default-alpn" }, + { key: "port", value: 8888 }, + { key: "ipv4hint", value: "1.2.3.4" }, + { key: "echconfig", value: "123..." }, + { key: "ipv6hint", value: "::1" }, + ], + }, + }, + ], + }); + + ({ inStatus } = await new TRRDNSListener("no-ip-host.com", { + expectedSuccess: false, + })); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + // Test CNAME/AliasForm loop + await trrServer.registerDoHAnswers("loop.com", "HTTPS", { + answers: [ + { + name: "loop.com", + type: "CNAME", + ttl: 55, + class: "IN", + flush: false, + data: "loop2.com", + }, + ], + }); + await trrServer.registerDoHAnswers("loop2.com", "HTTPS", { + answers: [ + { + name: "loop2.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 0, + name: "loop.com", + values: [], + }, + }, + ], + }); + + // Make sure these are the first requests + Assert.equal(await trrServer.requestCount("loop.com", "HTTPS"), 0); + Assert.equal(await trrServer.requestCount("loop2.com", "HTTPS"), 0); + + ({ inStatus } = await new TRRDNSListener("loop.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + expectedSuccess: false, + })); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + // Make sure the error was actually triggered by a loop. + Assert.greater(await trrServer.requestCount("loop.com", "HTTPS"), 2); + Assert.greater(await trrServer.requestCount("loop2.com", "HTTPS"), 2); + + // Alias form for . + await trrServer.registerDoHAnswers("empty.com", "A", { + answers: [ + { + name: "empty.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 0, + name: "", // This is not allowed + values: [], + }, + }, + ], + }); + + ({ inStatus } = await new TRRDNSListener("empty.com", { + expectedSuccess: false, + })); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + // We should ignore ServiceForm if an AliasForm record is also present + await trrServer.registerDoHAnswers("multi.com", "HTTPS", { + answers: [ + { + name: "multi.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "h3pool", + values: [ + { key: "alpn", value: ["h2", "h3"] }, + { key: "no-default-alpn" }, + { key: "port", value: 8888 }, + { key: "ipv4hint", value: "1.2.3.4" }, + { key: "echconfig", value: "123..." }, + { key: "ipv6hint", value: "::1" }, + ], + }, + }, + { + name: "multi.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 0, + name: "example.com", + values: [], + }, + }, + ], + }); + + let { inStatus: inStatus2 } = await new TRRDNSListener("multi.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + expectedSuccess: false, + }); + Assert.ok( + !Components.isSuccessCode(inStatus2), + `${inStatus2} should be an error code` + ); + + // the svcparam keys are in reverse order + await trrServer.registerDoHAnswers("order.com", "HTTPS", { + answers: [ + { + name: "order.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "h3pool", + values: [ + { key: "ipv6hint", value: "::1" }, + { key: "echconfig", value: "123..." }, + { key: "ipv4hint", value: "1.2.3.4" }, + { key: "port", value: 8888 }, + { key: "no-default-alpn" }, + { key: "alpn", value: ["h2", "h3"] }, + ], + }, + }, + ], + }); + + ({ inStatus: inStatus2 } = await new TRRDNSListener("order.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + expectedSuccess: false, + })); + Assert.ok( + !Components.isSuccessCode(inStatus2), + `${inStatus2} should be an error code` + ); + + // duplicate svcparam keys + await trrServer.registerDoHAnswers("duplicate.com", "HTTPS", { + answers: [ + { + name: "duplicate.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "h3pool", + values: [ + { key: "alpn", value: ["h2", "h3"] }, + { key: "alpn", value: ["h2", "h3", "h4"] }, + ], + }, + }, + ], + }); + + ({ inStatus: inStatus2 } = await new TRRDNSListener("duplicate.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + expectedSuccess: false, + })); + Assert.ok( + !Components.isSuccessCode(inStatus2), + `${inStatus2} should be an error code` + ); + + // mandatory svcparam + await trrServer.registerDoHAnswers("mandatory.com", "HTTPS", { + answers: [ + { + name: "mandatory.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "h3pool", + values: [ + { key: "mandatory", value: ["key100"] }, + { key: "alpn", value: ["h2", "h3"] }, + { key: "key100" }, + ], + }, + }, + ], + }); + + ({ inStatus: inStatus2 } = await new TRRDNSListener("mandatory.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + expectedSuccess: false, + })); + Assert.ok(!Components.isSuccessCode(inStatus2), `${inStatus2} should fail`); + + // mandatory svcparam + await trrServer.registerDoHAnswers("mandatory2.com", "HTTPS", { + answers: [ + { + name: "mandatory2.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "h3pool", + values: [ + { + key: "mandatory", + value: [ + "alpn", + "no-default-alpn", + "port", + "ipv4hint", + "echconfig", + "ipv6hint", + ], + }, + { key: "alpn", value: ["h2", "h3"] }, + { key: "no-default-alpn" }, + { key: "port", value: 8888 }, + { key: "ipv4hint", value: "1.2.3.4" }, + { key: "echconfig", value: "123..." }, + { key: "ipv6hint", value: "::1" }, + ], + }, + }, + ], + }); + + ({ inStatus: inStatus2 } = await new TRRDNSListener("mandatory2.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + })); + + Assert.ok(Components.isSuccessCode(inStatus2), `${inStatus2} should succeed`); + + // alias-mode with . targetName + await trrServer.registerDoHAnswers("no-alias.com", "HTTPS", { + answers: [ + { + name: "no-alias.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 0, + name: ".", + values: [], + }, + }, + ], + }); + + ({ inStatus: inStatus2 } = await new TRRDNSListener("no-alias.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + expectedSuccess: false, + })); + + Assert.ok(!Components.isSuccessCode(inStatus2), `${inStatus2} should fail`); + + // service-mode with . targetName + await trrServer.registerDoHAnswers("service.com", "HTTPS", { + answers: [ + { + name: "service.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: ".", + values: [{ key: "alpn", value: ["h2", "h3"] }], + }, + }, + ], + }); + + ({ inRecord, inStatus: inStatus2 } = await new TRRDNSListener("service.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + })); + Assert.ok(Components.isSuccessCode(inStatus2), `${inStatus2} should work`); + answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer[0].priority, 1); + Assert.equal(answer[0].name, "service.com"); +}); + +add_task(async function testNegativeResponse() { + let { inStatus } = await new TRRDNSListener("negative_test.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + expectedSuccess: false, + }); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + await trrServer.registerDoHAnswers("negative_test.com", "HTTPS", { + answers: [ + { + name: "negative_test.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "negative_test.com", + values: [{ key: "alpn", value: ["h2", "h3"] }], + }, + }, + ], + }); + + // Should still be failed because a negative response is from DNS cache. + ({ inStatus } = await new TRRDNSListener("negative_test.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + expectedSuccess: false, + })); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + if (inChildProcess()) { + do_send_remote_message("clearCache"); + await do_await_remote_message("clearCache-done"); + } else { + Services.dns.clearCache(true); + } + + let inRecord; + ({ inRecord, inStatus } = await new TRRDNSListener("negative_test.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + })); + Assert.ok(Components.isSuccessCode(inStatus), `${inStatus} should work`); + let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer[0].priority, 1); + Assert.equal(answer[0].name, "negative_test.com"); +}); + +add_task(async function testPortPrefixedName() { + if (inChildProcess()) { + do_send_remote_message("set-port-prefixed-pref"); + await do_await_remote_message("set-port-prefixed-pref-done"); + } else { + Services.prefs.setBoolPref( + "network.dns.port_prefixed_qname_https_rr", + true + ); + } + + await trrServer.registerDoHAnswers( + "_4433._https.port_prefix.test.com", + "HTTPS", + { + answers: [ + { + name: "_4433._https.port_prefix.test.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "port_prefix.test1.com", + values: [{ key: "alpn", value: ["h2", "h3"] }], + }, + }, + ], + } + ); + + let { inRecord, inStatus } = await new TRRDNSListener( + "port_prefix.test.com", + { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + port: 4433, + } + ); + Assert.ok(Components.isSuccessCode(inStatus), `${inStatus} should work`); + let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer[0].priority, 1); + Assert.equal(answer[0].name, "port_prefix.test1.com"); + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_trr_nat64.js b/netwerk/test/unit/test_trr_nat64.js new file mode 100644 index 0000000000..0c8caa87ec --- /dev/null +++ b/netwerk/test/unit/test_trr_nat64.js @@ -0,0 +1,120 @@ +/* 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 override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); + +trr_test_setup(); +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.connectivity-service.nat64-prefix"); + override.clearOverrides(); + trr_clear_prefs(); +}); + +/** + * Waits for an observer notification to fire. + * + * @param {String} topic The notification topic. + * @returns {Promise} A promise that fulfills when the notification is fired. + */ +function promiseObserverNotification(topic, matchFunc) { + return new Promise((resolve, reject) => { + Services.obs.addObserver(function observe(subject, topic, data) { + let matches = typeof matchFunc != "function" || matchFunc(subject, data); + if (!matches) { + return; + } + Services.obs.removeObserver(observe, topic); + resolve({ subject, data }); + }, topic); + }); +} + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish)); + }); +} + +add_task(async function test_add_nat64_prefix_to_trr() { + let trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + dump(`port = ${trrServer.port}\n`); + let chan = makeChan(`https://localhost:${trrServer.port}/test?bla=some`); + let [, resp] = await channelOpenPromise(chan); + equal(resp, "<h1> 404 Path not found: /test?bla=some</h1>"); + Services.dns.clearCache(true); + override.addIPOverride("ipv4only.arpa", "fe80::9b2b:c000:00aa"); + Services.prefs.setCharPref( + "network.connectivity-service.nat64-prefix", + "ae80::3b1b:c343:1133" + ); + + let topic = "network:connectivity-service:dns-checks-complete"; + if (mozinfo.socketprocess_networking) { + topic += "-from-socket-process"; + } + let notification = promiseObserverNotification(topic); + Services.obs.notifyObservers(null, "network:captive-portal-connectivity"); + await notification; + + Services.prefs.setIntPref("network.trr.mode", 2); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("xyz.foo", "A", { + answers: [ + { + name: "xyz.foo", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + let { inRecord } = await new TRRDNSListener("xyz.foo", { + expectedSuccess: false, + }); + + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + Assert.equal( + inRecord.getNextAddrAsString(), + "1.2.3.4", + `Checking that native IPv4 addresses have higher priority.` + ); + + Assert.equal( + inRecord.getNextAddrAsString(), + "ae80::3b1b:102:304", + `Checking the manually entered NAT64-prefixed address is in the middle.` + ); + + Assert.equal( + inRecord.getNextAddrAsString(), + "fe80::9b2b:102:304", + `Checking that the NAT64-prefixed address is appended at the back.` + ); + + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_trr_noPrefetch.js b/netwerk/test/unit/test_trr_noPrefetch.js new file mode 100644 index 0000000000..32b0847a46 --- /dev/null +++ b/netwerk/test/unit/test_trr_noPrefetch.js @@ -0,0 +1,174 @@ +/* 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"; + +let trrServer = null; +add_setup(async function setup() { + if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { + return; + } + + trr_test_setup(); + Services.prefs.setBoolPref("network.dns.disablePrefetch", true); + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.dns.disablePrefetch"); + trr_clear_prefs(); + }); + + trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + // We need to define both A and AAAA responses, otherwise + // we might race and pick up the skip reason for the other request. + await trrServer.registerDoHAnswers(`myfoo.test`, "A", { + answers: [], + }); + await trrServer.registerDoHAnswers(`myfoo.test`, "AAAA", { + answers: [], + }); + + // myfoo2.test will return sever error as it's not defined + + // return nxdomain for this one + await trrServer.registerDoHAnswers(`myfoo3.test`, "A", { + flags: 0x03, + answers: [], + }); + await trrServer.registerDoHAnswers(`myfoo3.test`, "AAAA", { + flags: 0x03, + answers: [], + }); + + await trrServer.registerDoHAnswers(`alt1.example.com`, "A", { + answers: [ + { + name: "alt1.example.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); +}); + +add_task(async function test_failure() { + let req = await new Promise(resolve => { + let chan = NetUtil.newChannel({ + uri: `http://myfoo.test/`, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)); + }); + + equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST); + equal( + req.QueryInterface(Ci.nsIHttpChannelInternal).effectiveTRRMode, + Ci.nsIRequest.TRR_ONLY_MODE + ); + equal( + req.QueryInterface(Ci.nsIHttpChannelInternal).trrSkipReason, + Ci.nsITRRSkipReason.TRR_NO_ANSWERS + ); + + req = await new Promise(resolve => { + let chan = NetUtil.newChannel({ + uri: `http://myfoo2.test/`, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)); + }); + + equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST); + equal( + req.QueryInterface(Ci.nsIHttpChannelInternal).effectiveTRRMode, + Ci.nsIRequest.TRR_ONLY_MODE + ); + equal( + req.QueryInterface(Ci.nsIHttpChannelInternal).trrSkipReason, + Ci.nsITRRSkipReason.TRR_RCODE_FAIL + ); + + req = await new Promise(resolve => { + let chan = NetUtil.newChannel({ + uri: `http://myfoo3.test/`, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)); + }); + + equal(req.status, Cr.NS_ERROR_UNKNOWN_HOST); + equal( + req.QueryInterface(Ci.nsIHttpChannelInternal).effectiveTRRMode, + Ci.nsIRequest.TRR_ONLY_MODE + ); + equal( + req.QueryInterface(Ci.nsIHttpChannelInternal).trrSkipReason, + Ci.nsITRRSkipReason.TRR_NXDOMAIN + ); +}); + +add_task(async function test_success() { + let httpServer = new NodeHTTP2Server(); + await httpServer.start(); + await httpServer.registerPathHandler("/", (req, resp) => { + resp.writeHead(200); + resp.end("done"); + }); + registerCleanupFunction(async () => { + await httpServer.stop(); + }); + + let req = await new Promise(resolve => { + let chan = NetUtil.newChannel({ + uri: `https://alt1.example.com:${httpServer.port()}/`, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + + equal(req.status, Cr.NS_OK); + equal( + req.QueryInterface(Ci.nsIHttpChannelInternal).effectiveTRRMode, + Ci.nsIRequest.TRR_ONLY_MODE + ); + equal( + req.QueryInterface(Ci.nsIHttpChannelInternal).trrSkipReason, + Ci.nsITRRSkipReason.TRR_OK + ); + + // Another request to check connection reuse + req = await new Promise(resolve => { + let chan = NetUtil.newChannel({ + uri: `https://alt1.example.com:${httpServer.port()}/second`, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.asyncOpen(new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL)); + }); + + equal(req.status, Cr.NS_OK); + equal( + req.QueryInterface(Ci.nsIHttpChannelInternal).effectiveTRRMode, + Ci.nsIRequest.TRR_ONLY_MODE + ); + equal( + req.QueryInterface(Ci.nsIHttpChannelInternal).trrSkipReason, + Ci.nsITRRSkipReason.TRR_OK + ); +}); diff --git a/netwerk/test/unit/test_trr_proxy.js b/netwerk/test/unit/test_trr_proxy.js new file mode 100644 index 0000000000..19a85c14a4 --- /dev/null +++ b/netwerk/test/unit/test_trr_proxy.js @@ -0,0 +1,154 @@ +// These are globlas defined for proxy servers, in ProxyAutoConfig.cpp. See +// PACGlobalFunctions +/* globals dnsResolve, alert */ + +/* This test checks that using a PAC script still works when TRR is on. + Steps: + - Set the pac script + - Do a request to make sure that the script is loaded + - Set the TRR mode + - Make a request that would lead to running the PAC script + We run these steps for TRR mode 2 and 3, and with fetchOffMainThread = true/false +*/ + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.proxy.type"); + Services.prefs.clearUserPref("network.proxy.parse_pac_on_socket_process"); + trr_clear_prefs(); +}); + +function FindProxyForURL(url, host) { + alert(`PAC resolving: ${host}`); + alert(dnsResolve(host)); + return "DIRECT"; +} + +XPCOMUtils.defineLazyGetter(this, "systemSettings", function () { + return { + QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]), + + mainThreadOnly: true, + PACURI: `data:application/x-ns-proxy-autoconfig;charset=utf-8,${encodeURIComponent( + FindProxyForURL.toString() + )}`, + getProxyForURI(aURI) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + }; +}); + +const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); + +async function do_test_pac_dnsResolve() { + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + Services.console.reset(); + // Create a console listener. + let consolePromise = new Promise(resolve => { + let listener = { + observe(message) { + // Ignore unexpected messages. + if (!(message instanceof Ci.nsIConsoleMessage)) { + return; + } + + if (message.message.includes("PAC file installed from")) { + Services.console.unregisterListener(listener); + resolve(); + } + }, + }; + + Services.console.registerListener(listener); + }); + + MockRegistrar.register( + "@mozilla.org/system-proxy-settings;1", + systemSettings + ); + Services.prefs.setIntPref( + "network.proxy.type", + Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM + ); + + let httpserv = new HttpServer(); + httpserv.registerPathHandler("/", function handler(metadata, response) { + let content = "ok"; + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + }); + httpserv.start(-1); + + Services.prefs.setBoolPref("network.dns.native-is-localhost", false); + Services.prefs.setIntPref("network.trr.mode", 0); // Disable TRR until the PAC is loaded + override.addIPOverride("example.org", "127.0.0.1"); + let chan = NetUtil.newChannel({ + uri: `http://example.org:${httpserv.identity.primaryPort}/`, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + await consolePromise; + + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + override.addIPOverride("foo.example.com", "127.0.0.1"); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=127.0.0.1` + ); + + trr_test_setup(); + + async function test_with(DOMAIN, trrMode, fetchOffMainThread) { + Services.prefs.setIntPref("network.trr.mode", trrMode); // TRR first + Services.prefs.setBoolPref( + "network.trr.fetch_off_main_thread", + fetchOffMainThread + ); + override.addIPOverride(DOMAIN, "127.0.0.1"); + + chan = NetUtil.newChannel({ + uri: `http://${DOMAIN}:${httpserv.identity.primaryPort}/`, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + + await override.clearHostOverride(DOMAIN); + } + + await test_with("test1.com", 2, true); + await test_with("test2.com", 3, true); + await test_with("test3.com", 2, false); + await test_with("test4.com", 3, false); + await httpserv.stop(); +} + +add_task(async function test_pac_dnsResolve() { + Services.prefs.setBoolPref( + "network.proxy.parse_pac_on_socket_process", + false + ); + + await do_test_pac_dnsResolve(); + + if (mozinfo.socketprocess_networking) { + info("run test again"); + Services.prefs.clearUserPref("network.proxy.type"); + trr_clear_prefs(); + Services.prefs.setBoolPref( + "network.proxy.parse_pac_on_socket_process", + true + ); + Services.prefs.setIntPref("network.proxy.type", 2); + Services.prefs.setIntPref("network.proxy.type", 0); + await do_test_pac_dnsResolve(); + } +}); diff --git a/netwerk/test/unit/test_trr_proxy_auth.js b/netwerk/test/unit/test_trr_proxy_auth.js new file mode 100644 index 0000000000..0576514486 --- /dev/null +++ b/netwerk/test/unit/test_trr_proxy_auth.js @@ -0,0 +1,122 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ + +function setup() { + trr_test_setup(); + Services.prefs.setBoolPref("network.trr.fetch_off_main_thread", true); +} + +setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); +}); + +function AuthPrompt() {} + +AuthPrompt.prototype = { + user: "guest", + pass: "guest", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + + promptAuth: function ap_promptAuth(channel, level, authInfo) { + authInfo.username = this.user; + authInfo.password = this.pass; + + return true; + }, + + asyncPromptAuth: function ap_async(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +function Requestor() {} + +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface: function requestor_gi(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + // Allow the prompt to store state by caching it here + if (!this.prompt) { + this.prompt = new AuthPrompt(); + } + return this.prompt; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompt: null, +}; + +// Test if we successfully retry TRR request on main thread. +add_task(async function test_trr_proxy_auth() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + let trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.proxy.com", "A", { + answers: [ + { + name: "test.proxy.com", + ttl: 55, + type: "A", + flush: false, + data: "3.3.3.3", + }, + ], + }); + + await new TRRDNSListener("test.proxy.com", "3.3.3.3"); + + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(0, true); + registerCleanupFunction(async () => { + await proxy.stop(); + await trrServer.stop(); + }); + + let authTriggered = false; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == "http-on-examine-response") { + Services.obs.removeObserver(observer, "http-on-examine-response"); + let channel = aSubject.QueryInterface(Ci.nsIChannel); + channel.notificationCallbacks = new Requestor(); + if ( + channel.URI.spec.startsWith( + `https://foo.example.com:${trrServer.port}/dns-query` + ) + ) { + authTriggered = true; + } + } + }, + }; + Services.obs.addObserver(observer, "http-on-examine-response"); + + Services.dns.clearCache(true); + await new TRRDNSListener("test.proxy.com", "3.3.3.3"); + Assert.ok(authTriggered); +}); diff --git a/netwerk/test/unit/test_trr_strict_mode.js b/netwerk/test/unit/test_trr_strict_mode.js new file mode 100644 index 0000000000..7cd16550fa --- /dev/null +++ b/netwerk/test/unit/test_trr_strict_mode.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"; + +function setup() { + trr_test_setup(); +} +setup(); + +add_task(async function checkBlocklisting() { + Services.prefs.setBoolPref("network.trr.temp_blocklist", true); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", true); + + let trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + info(`port = ${trrServer.port}\n`); + + Services.dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); + + // Check that we properly fallback to native DNS for a variety of DNS rcodes + for (let i = 0; i <= 5; i++) { + info(`testing rcode=${i}`); + await trrServer.registerDoHAnswers(`sub${i}.blocklisted.com`, "A", { + flags: i, + }); + await trrServer.registerDoHAnswers(`sub${i}.blocklisted.com`, "AAAA", { + flags: i, + }); + + await new TRRDNSListener(`sub${i}.blocklisted.com`, { + expectedAnswer: "127.0.0.1", + }); + Services.dns.clearCache(true); + await new TRRDNSListener(`sub${i}.blocklisted.com`, { + expectedAnswer: "127.0.0.1", + }); + await new TRRDNSListener(`sub.sub${i}.blocklisted.com`, { + expectedAnswer: "127.0.0.1", + }); + Services.dns.clearCache(true); + } +}); diff --git a/netwerk/test/unit/test_trr_telemetry.js b/netwerk/test/unit/test_trr_telemetry.js new file mode 100644 index 0000000000..69f5d59201 --- /dev/null +++ b/netwerk/test/unit/test_trr_telemetry.js @@ -0,0 +1,59 @@ +"use strict"; + +/* import-globals-from trr_common.js */ + +// Allow telemetry probes which may otherwise be disabled for some +// applications (e.g. Thunderbird). +Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true +); + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +function setup() { + h2Port = trr_test_setup(); +} + +let TRR_OK = 1; + +setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); +}); + +async function trrLookup(mode, rolloutMode) { + let hist = TelemetryTestUtils.getAndClearKeyedHistogram( + "TRR_SKIP_REASON_TRR_FIRST2" + ); + + if (rolloutMode) { + info("Testing doh-rollout.mode"); + setModeAndURI(0, "doh?responseIP=2.2.2.2"); + Services.prefs.setIntPref("doh-rollout.mode", rolloutMode); + } else { + setModeAndURI(mode, "doh?responseIP=2.2.2.2"); + } + + Services.dns.clearCache(true); + await new TRRDNSListener("test.example.com", "2.2.2.2"); + let expectedKey = `(other)_${mode}`; + if (mode == 0) { + expectedKey = "(other)"; + } + TelemetryTestUtils.assertKeyedHistogramValue(hist, expectedKey, TRR_OK, 1); +} + +add_task(async function test_trr_lookup_mode_2() { + await trrLookup(2); +}); + +add_task(async function test_trr_lookup_mode_3() { + await trrLookup(3); +}); + +add_task(async function test_trr_lookup_mode_0() { + await trrLookup(0, 2); +}); diff --git a/netwerk/test/unit/test_trr_ttl.js b/netwerk/test/unit/test_trr_ttl.js new file mode 100644 index 0000000000..77e1506555 --- /dev/null +++ b/netwerk/test/unit/test_trr_ttl.js @@ -0,0 +1,60 @@ +/* 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"; + +trr_test_setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); +}); + +let trrServer = null; +add_task(async function setup() { + trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + dump(`port = ${trrServer.port}\n`); + + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST); +}); + +add_task(async function check_ttl_works() { + await trrServer.registerDoHAnswers("example.com", "A", { + answers: [ + { + name: "example.com", + ttl: 55, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + let { inRecord } = await new TRRDNSListener("example.com", { + expectedAnswer: "1.2.3.4", + }); + equal(inRecord.QueryInterface(Ci.nsIDNSAddrRecord).ttl, 55); + await trrServer.registerDoHAnswers("example.org", "A", { + answers: [ + { + name: "example.org", + ttl: 999, + type: "A", + flush: false, + data: "1.2.3.4", + }, + ], + }); + // Simple check to see that TRR works. + ({ inRecord } = await new TRRDNSListener("example.org", { + expectedAnswer: "1.2.3.4", + })); + equal(inRecord.QueryInterface(Ci.nsIDNSAddrRecord).ttl, 999); +}); diff --git a/netwerk/test/unit/test_trr_with_proxy.js b/netwerk/test/unit/test_trr_with_proxy.js new file mode 100644 index 0000000000..c3a3ef9d4e --- /dev/null +++ b/netwerk/test/unit/test_trr_with_proxy.js @@ -0,0 +1,201 @@ +/* This test checks that a TRRServiceChannel can connect to the server with + a proxy. + Steps: + - Setup the proxy (PAC, proxy filter, and system proxy settings) + - Test when "network.trr.async_connInfo" is false. In this case, every + TRRServicChannel waits for the proxy info to be resolved. + - Test when "network.trr.async_connInfo" is true. In this case, every + TRRServicChannel uses an already created connection info to connect. + - The test test_trr_uri_change() is about checking if trr connection info + is updated correctly when trr uri changed. +*/ + +"use strict"; + +/* import-globals-from trr_common.js */ + +let filter; +let systemProxySettings; +let trrProxy; +const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + +function setup() { + h2Port = trr_test_setup(); + SetParentalControlEnabled(false); +} + +setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.proxy.type"); + Services.prefs.clearUserPref("network.proxy.autoconfig_url"); + Services.prefs.clearUserPref("network.trr.async_connInfo"); + if (trrProxy) { + await trrProxy.stop(); + } +}); + +class ProxyFilter { + constructor(type, host, port, flags) { + this._type = type; + this._host = host; + this._port = port; + this._flags = flags; + this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]); + } + applyFilter(uri, pi, cb) { + cb.onProxyFilterResult( + pps.newProxyInfo( + this._type, + this._host, + this._port, + "", + "", + this._flags, + 1000, + null + ) + ); + } +} + +async function doTest(proxySetup, delay) { + info("Verifying a basic A record"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); // TRR-first + + trrProxy = new TRRProxy(); + await trrProxy.start(h2Port); + info("port=" + trrProxy.port); + + await proxySetup(trrProxy.port); + + if (delay) { + await new Promise(resolve => do_timeout(delay, resolve)); + } + + await new TRRDNSListener("bar.example.com", "2.2.2.2"); + + // Session count is 2 because of we send two TRR queries (A and AAAA). + Assert.equal( + await trrProxy.proxy_session_counter(), + 2, + `Session count should be 2` + ); + + // clean up + Services.prefs.clearUserPref("network.proxy.type"); + Services.prefs.clearUserPref("network.proxy.autoconfig_url"); + if (filter) { + pps.unregisterFilter(filter); + filter = null; + } + if (systemProxySettings) { + MockRegistrar.unregister(systemProxySettings); + systemProxySettings = null; + } + + await trrProxy.stop(); + trrProxy = null; +} + +add_task(async function test_trr_proxy() { + async function setupPACWithDataURL(proxyPort) { + var pac = `data:text/plain, function FindProxyForURL(url, host) { return "HTTPS foo.example.com:${proxyPort}";}`; + Services.prefs.setIntPref("network.proxy.type", 2); + Services.prefs.setCharPref("network.proxy.autoconfig_url", pac); + } + + async function setupPACWithHttpURL(proxyPort) { + let httpserv = new HttpServer(); + httpserv.registerPathHandler("/", function handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + let content = `function FindProxyForURL(url, host) { return "HTTPS foo.example.com:${proxyPort}";}`; + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + }); + httpserv.start(-1); + Services.prefs.setIntPref("network.proxy.type", 2); + let pacUri = `http://127.0.0.1:${httpserv.identity.primaryPort}/`; + Services.prefs.setCharPref("network.proxy.autoconfig_url", pacUri); + + function consoleMessageObserved() { + return new Promise(resolve => { + let listener = { + QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener"]), + observe(msg) { + if (msg == `PAC file installed from ${pacUri}`) { + Services.console.unregisterListener(listener); + resolve(); + } + }, + }; + Services.console.registerListener(listener); + }); + } + + await consoleMessageObserved(); + } + + async function setupProxyFilter(proxyPort) { + filter = new ProxyFilter("https", "foo.example.com", proxyPort, 0); + pps.registerFilter(filter, 10); + } + + async function setupSystemProxySettings(proxyPort) { + systemProxySettings = { + QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]), + mainThreadOnly: true, + PACURI: null, + getProxyForURI: (aSpec, aScheme, aHost, aPort) => { + return `HTTPS foo.example.com:${proxyPort}`; + }, + }; + + MockRegistrar.register( + "@mozilla.org/system-proxy-settings;1", + systemProxySettings + ); + + Services.prefs.setIntPref( + "network.proxy.type", + Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM + ); + + // simulate that system proxy setting is changed. + pps.notifyProxyConfigChangedInternal(); + } + + Services.prefs.setBoolPref("network.trr.async_connInfo", false); + await doTest(setupPACWithDataURL); + await doTest(setupPACWithDataURL, 1000); + await doTest(setupPACWithHttpURL); + await doTest(setupPACWithHttpURL, 1000); + await doTest(setupProxyFilter); + await doTest(setupProxyFilter, 1000); + await doTest(setupSystemProxySettings); + await doTest(setupSystemProxySettings, 1000); + + Services.prefs.setBoolPref("network.trr.async_connInfo", true); + await doTest(setupPACWithDataURL); + await doTest(setupPACWithDataURL, 1000); + await doTest(setupPACWithHttpURL); + await doTest(setupPACWithHttpURL, 1000); + await doTest(setupProxyFilter); + await doTest(setupProxyFilter, 1000); + await doTest(setupSystemProxySettings); + await doTest(setupSystemProxySettings, 1000); +}); + +add_task(async function test_trr_uri_change() { + Services.prefs.setIntPref("network.proxy.type", 0); + Services.prefs.setBoolPref("network.trr.async_connInfo", true); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2", "127.0.0.1"); + + await new TRRDNSListener("car.example.com", "127.0.0.1"); + + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + await new TRRDNSListener("car.example.net", "2.2.2.2"); +}); diff --git a/netwerk/test/unit/test_udp_multicast.js b/netwerk/test/unit/test_udp_multicast.js new file mode 100644 index 0000000000..305c916aa7 --- /dev/null +++ b/netwerk/test/unit/test_udp_multicast.js @@ -0,0 +1,110 @@ +// Bug 960397: UDP multicast options +"use strict"; + +var { Constructor: CC } = Components; + +const UDPSocket = CC( + "@mozilla.org/network/udp-socket;1", + "nsIUDPSocket", + "init" +); +const ADDRESS_TEST1 = "224.0.0.200"; +const ADDRESS_TEST2 = "224.0.0.201"; +const ADDRESS_TEST3 = "224.0.0.202"; +const ADDRESS_TEST4 = "224.0.0.203"; + +const TIMEOUT = 2000; + +var gConverter; + +function run_test() { + setup(); + run_next_test(); +} + +function setup() { + gConverter = Cc[ + "@mozilla.org/intl/scriptableunicodeconverter" + ].createInstance(Ci.nsIScriptableUnicodeConverter); + gConverter.charset = "utf8"; +} + +function createSocketAndJoin(addr) { + let socket = new UDPSocket( + -1, + false, + Services.scriptSecurityManager.getSystemPrincipal() + ); + socket.joinMulticast(addr); + return socket; +} + +function sendPing(socket, addr) { + let ping = "ping"; + let rawPing = gConverter.convertToByteArray(ping); + + return new Promise((resolve, reject) => { + socket.asyncListen({ + onPacketReceived(s, message) { + info("Received on port " + socket.port); + Assert.equal(message.data, ping); + socket.close(); + resolve(message.data); + }, + onStopListening(socket, status) {}, + }); + + info("Multicast send to port " + socket.port); + socket.send(addr, socket.port, rawPing, rawPing.length); + + // Timers are bad, but it seems like the only way to test *not* getting a + // packet. + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + () => { + socket.close(); + reject(); + }, + TIMEOUT, + Ci.nsITimer.TYPE_ONE_SHOT + ); + }); +} + +add_test(() => { + info("Joining multicast group"); + let socket = createSocketAndJoin(ADDRESS_TEST1); + sendPing(socket, ADDRESS_TEST1).then(run_next_test, () => + do_throw("Joined group, but no packet received") + ); +}); + +add_test(() => { + info("Disabling multicast loopback"); + let socket = createSocketAndJoin(ADDRESS_TEST2); + socket.multicastLoopback = false; + sendPing(socket, ADDRESS_TEST2).then( + () => do_throw("Loopback disabled, but still got a packet"), + run_next_test + ); +}); + +add_test(() => { + info("Changing multicast interface"); + let socket = createSocketAndJoin(ADDRESS_TEST3); + socket.multicastInterface = "127.0.0.1"; + sendPing(socket, ADDRESS_TEST3).then( + () => do_throw("Changed interface, but still got a packet"), + run_next_test + ); +}); + +add_test(() => { + info("Leaving multicast group"); + let socket = createSocketAndJoin(ADDRESS_TEST4); + socket.leaveMulticast(ADDRESS_TEST4); + sendPing(socket, ADDRESS_TEST4).then( + () => do_throw("Left group, but still got a packet"), + run_next_test + ); +}); diff --git a/netwerk/test/unit/test_udpsocket.js b/netwerk/test/unit/test_udpsocket.js new file mode 100644 index 0000000000..340fe6aa13 --- /dev/null +++ b/netwerk/test/unit/test_udpsocket.js @@ -0,0 +1,89 @@ +/* -*- Mode: Javascript; indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 HELLO_WORLD = "Hello World"; +const EMPTY_MESSAGE = ""; + +add_test(function test_udp_message_raw_data() { + info("test for nsIUDPMessage.rawData"); + + let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance( + Ci.nsIUDPSocket + ); + + socket.init(-1, true, Services.scriptSecurityManager.getSystemPrincipal()); + info("Port assigned : " + socket.port); + socket.asyncListen({ + QueryInterface: ChromeUtils.generateQI(["nsIUDPSocketListener"]), + onPacketReceived(aSocket, aMessage) { + let recv_data = String.fromCharCode.apply(null, aMessage.rawData); + Assert.equal(recv_data, HELLO_WORLD); + Assert.equal(recv_data, aMessage.data); + socket.close(); + run_next_test(); + }, + onStopListening(aSocket, aStatus) {}, + }); + + let rawData = new Uint8Array(HELLO_WORLD.length); + for (let i = 0; i < HELLO_WORLD.length; i++) { + rawData[i] = HELLO_WORLD.charCodeAt(i); + } + let written = socket.send("127.0.0.1", socket.port, rawData); + Assert.equal(written, HELLO_WORLD.length); +}); + +add_test(function test_udp_send_stream() { + info("test for nsIUDPSocket.sendBinaryStream"); + + let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance( + Ci.nsIUDPSocket + ); + + socket.init(-1, true, Services.scriptSecurityManager.getSystemPrincipal()); + socket.asyncListen({ + QueryInterface: ChromeUtils.generateQI(["nsIUDPSocketListener"]), + onPacketReceived(aSocket, aMessage) { + let recv_data = String.fromCharCode.apply(null, aMessage.rawData); + Assert.equal(recv_data, HELLO_WORLD); + socket.close(); + run_next_test(); + }, + onStopListening(aSocket, aStatus) {}, + }); + + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setData(HELLO_WORLD, HELLO_WORLD.length); + socket.sendBinaryStream("127.0.0.1", socket.port, stream); +}); + +add_test(function test_udp_message_zero_length() { + info("test for nsIUDPMessage with zero length"); + + let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance( + Ci.nsIUDPSocket + ); + + socket.init(-1, true, Services.scriptSecurityManager.getSystemPrincipal()); + info("Port assigned : " + socket.port); + socket.asyncListen({ + QueryInterface: ChromeUtils.generateQI(["nsIUDPSocketListener"]), + onPacketReceived(aSocket, aMessage) { + let recv_data = String.fromCharCode.apply(null, aMessage.rawData); + Assert.equal(recv_data, EMPTY_MESSAGE); + Assert.equal(recv_data, aMessage.data); + socket.close(); + run_next_test(); + }, + onStopListening(aSocket, aStatus) {}, + }); + + let rawData = new Uint8Array(EMPTY_MESSAGE.length); + let written = socket.send("127.0.0.1", socket.port, rawData); + Assert.equal(written, EMPTY_MESSAGE.length); +}); diff --git a/netwerk/test/unit/test_udpsocket_offline.js b/netwerk/test/unit/test_udpsocket_offline.js new file mode 100644 index 0000000000..080f54281d --- /dev/null +++ b/netwerk/test/unit/test_udpsocket_offline.js @@ -0,0 +1,144 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_test(function test_ipv4_any() { + let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance( + Ci.nsIUDPSocket + ); + + Assert.throws(() => { + socket.init(-1, false, Services.scriptSecurityManager.getSystemPrincipal()); + }, /NS_ERROR_OFFLINE/); + + run_next_test(); +}); + +add_test(function test_ipv6_any() { + let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance( + Ci.nsIUDPSocket + ); + + Assert.throws(() => { + socket.init2("::", -1, Services.scriptSecurityManager.getSystemPrincipal()); + }, /NS_ERROR_OFFLINE/); + + run_next_test(); +}); + +add_test(function test_ipv4() { + let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance( + Ci.nsIUDPSocket + ); + + Assert.throws(() => { + socket.init2( + "240.0.0.1", + -1, + Services.scriptSecurityManager.getSystemPrincipal() + ); + }, /NS_ERROR_OFFLINE/); + + run_next_test(); +}); + +add_test(function test_ipv6() { + let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance( + Ci.nsIUDPSocket + ); + + Assert.throws(() => { + socket.init2( + "2001:db8::1", + -1, + Services.scriptSecurityManager.getSystemPrincipal() + ); + }, /NS_ERROR_OFFLINE/); + + run_next_test(); +}); + +add_test(function test_ipv4_loopback() { + let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance( + Ci.nsIUDPSocket + ); + + try { + socket.init2( + "127.0.0.1", + -1, + Services.scriptSecurityManager.getSystemPrincipal(), + true + ); + } catch (e) { + Assert.ok(false, "unexpected exception: " + e); + } + + // Now with localhost connections disabled in offline mode. + Services.prefs.setBoolPref("network.disable-localhost-when-offline", true); + socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance( + Ci.nsIUDPSocket + ); + + Assert.throws(() => { + socket.init2( + "127.0.0.1", + -1, + Services.scriptSecurityManager.getSystemPrincipal(), + true + ); + }, /NS_ERROR_OFFLINE/); + + Services.prefs.setBoolPref("network.disable-localhost-when-offline", false); + + run_next_test(); +}); + +add_test(function test_ipv6_loopback() { + let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance( + Ci.nsIUDPSocket + ); + + try { + socket.init2( + "::1", + -1, + Services.scriptSecurityManager.getSystemPrincipal(), + true + ); + } catch (e) { + Assert.ok(false, "unexpected exception: " + e); + } + + // Now with localhost connections disabled in offline mode. + Services.prefs.setBoolPref("network.disable-localhost-when-offline", true); + socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance( + Ci.nsIUDPSocket + ); + + Assert.throws(() => { + socket.init2( + "::1", + -1, + Services.scriptSecurityManager.getSystemPrincipal(), + true + ); + }, /NS_ERROR_OFFLINE/); + + Services.prefs.setBoolPref("network.disable-localhost-when-offline", false); + + run_next_test(); +}); + +function run_test() { + // jshint ignore:line + Services.io.offline = true; + Services.prefs.setBoolPref("network.disable-localhost-when-offline", false); + registerCleanupFunction(() => { + Services.io.offline = false; + Services.prefs.clearUserPref("network.disable-localhost-when-offline"); + }); + run_next_test(); +} diff --git a/netwerk/test/unit/test_unescapestring.js b/netwerk/test/unit/test_unescapestring.js new file mode 100644 index 0000000000..9853d2c0af --- /dev/null +++ b/netwerk/test/unit/test_unescapestring.js @@ -0,0 +1,35 @@ +"use strict"; + +const ONLY_NONASCII = Ci.nsINetUtil.ESCAPE_URL_ONLY_NONASCII; +const SKIP_CONTROL = Ci.nsINetUtil.ESCAPE_URL_SKIP_CONTROL; + +var tests = [ + ["foo", "foo", 0], + ["foo%20bar", "foo bar", 0], + ["foo%2zbar", "foo%2zbar", 0], + ["foo%", "foo%", 0], + ["%zzfoo", "%zzfoo", 0], + ["foo%z", "foo%z", 0], + ["foo%00bar", "foo\x00bar", 0], + ["foo%ffbar", "foo\xffbar", 0], + ["%00%1b%20%61%7f%80%ff", "%00%1b%20%61%7f\x80\xff", ONLY_NONASCII], + ["%00%1b%20%61%7f%80%ff", "%00%1b a%7f\x80\xff", SKIP_CONTROL], + [ + "%00%1b%20%61%7f%80%ff", + "%00%1b%20%61%7f\x80\xff", + ONLY_NONASCII | SKIP_CONTROL, + ], + // Test that we do not drop the high-bytes of a UTF-16 string. + ["\u30a8\u30c9", "\xe3\x82\xa8\xe3\x83\x89", 0], +]; + +function run_test() { + for (var i = 0; i < tests.length; ++i) { + dump("Test " + i + " (" + tests[i][0] + ", " + tests[i][2] + ")\n"); + Assert.equal( + Services.io.unescapeString(tests[i][0], tests[i][2]), + tests[i][1] + ); + } + dump(tests.length + " tests passed\n"); +} diff --git a/netwerk/test/unit/test_unix_domain.js b/netwerk/test/unit/test_unix_domain.js new file mode 100644 index 0000000000..ba1b28cf21 --- /dev/null +++ b/netwerk/test/unit/test_unix_domain.js @@ -0,0 +1,699 @@ +// Exercise Unix domain sockets. +"use strict"; + +var CC = Components.Constructor; + +const UnixServerSocket = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "initWithFilename" +); +const UnixAbstractServerSocket = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "initWithAbstractAddress" +); + +const ScriptableInputStream = CC( + "@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", + "init" +); + +const socketTransportService = Cc[ + "@mozilla.org/network/socket-transport-service;1" +].getService(Ci.nsISocketTransportService); + +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + +const allPermissions = parseInt("777", 8); + +function run_test() { + // If we're on Windows, simply check for graceful failure. + if (mozinfo.os == "win") { + test_not_supported(); + return; + } + + // The xpcshell temp directory on Android doesn't seem to let us create + // Unix domain sockets. (Perhaps it's a FAT filesystem?) + if (mozinfo.os != "android") { + add_test(test_echo); + add_test(test_name_too_long); + add_test(test_no_directory); + add_test(test_no_such_socket); + add_test(test_address_in_use); + add_test(test_file_in_way); + add_test(test_create_permission); + add_test(test_connect_permission); + add_test(test_long_socket_name); + add_test(test_keep_when_offline); + } + + if (mozinfo.os == "android" || mozinfo.os == "linux") { + add_test(test_abstract_address_socket); + } + + run_next_test(); +} + +// Check that creating a Unix domain socket fails gracefully on Windows. +function test_not_supported() { + let socketName = do_get_tempdir(); + socketName.append("socket"); + info("creating socket: " + socketName.path); + + do_check_throws_nsIException( + () => new UnixServerSocket(socketName, allPermissions, -1), + "NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED" + ); + + do_check_throws_nsIException( + () => socketTransportService.createUnixDomainTransport(socketName), + "NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED" + ); +} + +// Actually exchange data with Unix domain sockets. +function test_echo() { + let log = ""; + + let socketName = do_get_tempdir(); + socketName.append("socket"); + + // Create a server socket, listening for connections. + info("creating socket: " + socketName.path); + let server = new UnixServerSocket(socketName, allPermissions, -1); + server.asyncListen({ + onSocketAccepted(aServ, aTransport) { + info("called test_echo's onSocketAccepted"); + log += "a"; + + Assert.equal(aServ, server); + + let connection = aTransport; + + // Check the server socket's self address. + let connectionSelfAddr = connection.getScriptableSelfAddr(); + Assert.equal(connectionSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); + Assert.equal(connectionSelfAddr.address, socketName.path); + + // The client socket is anonymous, so the server transport should + // have an empty peer address. + Assert.equal(connection.host, ""); + Assert.equal(connection.port, 0); + let connectionPeerAddr = connection.getScriptablePeerAddr(); + Assert.equal(connectionPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); + Assert.equal(connectionPeerAddr.address, ""); + + let serverAsyncInput = connection + .openInputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncInputStream); + let serverOutput = connection.openOutputStream(0, 0, 0); + + serverAsyncInput.asyncWait( + function (aStream) { + info("called test_echo's server's onInputStreamReady"); + let serverScriptableInput = new ScriptableInputStream(aStream); + + // Receive data from the client, and send back a response. + Assert.equal( + serverScriptableInput.readBytes(17), + "Mervyn Murgatroyd" + ); + info("server has read message from client"); + serverOutput.write("Ruthven Murgatroyd", 18); + info("server has written to client"); + }, + 0, + 0, + threadManager.currentThread + ); + }, + + onStopListening(aServ, aStatus) { + info("called test_echo's onStopListening"); + log += "s"; + + Assert.equal(aServ, server); + Assert.equal(log, "acs"); + + run_next_test(); + }, + }); + + // Create a client socket, and connect to the server. + let client = socketTransportService.createUnixDomainTransport(socketName); + Assert.equal(client.host, socketName.path); + Assert.equal(client.port, 0); + + let clientAsyncInput = client + .openInputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncInputStream); + let clientInput = new ScriptableInputStream(clientAsyncInput); + let clientOutput = client.openOutputStream(0, 0, 0); + + clientOutput.write("Mervyn Murgatroyd", 17); + info("client has written to server"); + + clientAsyncInput.asyncWait( + function (aStream) { + info("called test_echo's client's onInputStreamReady"); + log += "c"; + + Assert.equal(aStream, clientAsyncInput); + + // Now that the connection has been established, we can check the + // transport's self and peer addresses. + let clientSelfAddr = client.getScriptableSelfAddr(); + Assert.equal(clientSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); + Assert.equal(clientSelfAddr.address, ""); + + Assert.equal(client.host, socketName.path); // re-check, but hey + let clientPeerAddr = client.getScriptablePeerAddr(); + Assert.equal(clientPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); + Assert.equal(clientPeerAddr.address, socketName.path); + + Assert.equal(clientInput.readBytes(18), "Ruthven Murgatroyd"); + info("client has read message from server"); + + server.close(); + }, + 0, + 0, + threadManager.currentThread + ); +} + +// Create client and server sockets using a path that's too long. +function test_name_too_long() { + let socketName = do_get_tempdir(); + // The length limits on all the systems NSPR supports are a bit past 100. + socketName.append(new Array(1000).join("x")); + + // The length must be checked before we ever make any system calls --- we + // have to create the sockaddr first --- so it's unambiguous which error + // we should get here. + + do_check_throws_nsIException( + () => new UnixServerSocket(socketName, 0, -1), + "NS_ERROR_FILE_NAME_TOO_LONG" + ); + + // Unlike most other client socket errors, this one gets reported + // immediately, as we can't even initialize the sockaddr with the given + // name. + do_check_throws_nsIException( + () => socketTransportService.createUnixDomainTransport(socketName), + "NS_ERROR_FILE_NAME_TOO_LONG" + ); + + run_next_test(); +} + +// Try creating a socket in a directory that doesn't exist. +function test_no_directory() { + let socketName = do_get_tempdir(); + socketName.append("missing"); + socketName.append("socket"); + + do_check_throws_nsIException( + () => new UnixServerSocket(socketName, 0, -1), + "NS_ERROR_FILE_NOT_FOUND" + ); + + run_next_test(); +} + +// Try connecting to a server socket that isn't there. +function test_no_such_socket() { + let socketName = do_get_tempdir(); + socketName.append("nonexistent-socket"); + + let client = socketTransportService.createUnixDomainTransport(socketName); + let clientAsyncInput = client + .openInputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncInputStream); + clientAsyncInput.asyncWait( + function (aStream) { + info("called test_no_such_socket's onInputStreamReady"); + + Assert.equal(aStream, clientAsyncInput); + + // nsISocketTransport puts off actually creating sockets as long as + // possible, so the error in connecting doesn't actually show up until + // this point. + do_check_throws_nsIException( + () => clientAsyncInput.available(), + "NS_ERROR_FILE_NOT_FOUND" + ); + + clientAsyncInput.close(); + client.close(Cr.NS_OK); + + run_next_test(); + }, + 0, + 0, + threadManager.currentThread + ); +} + +// Creating a socket with a name that another socket is already using is an +// error. +function test_address_in_use() { + let socketName = do_get_tempdir(); + socketName.append("socket-in-use"); + + // Create one server socket. + new UnixServerSocket(socketName, allPermissions, -1); + + // Now try to create another with the same name. + do_check_throws_nsIException( + () => new UnixServerSocket(socketName, allPermissions, -1), + "NS_ERROR_SOCKET_ADDRESS_IN_USE" + ); + + run_next_test(); +} + +// Creating a socket with a name that is already a file is an error. +function test_file_in_way() { + let socketName = do_get_tempdir(); + socketName.append("file_in_way"); + + // Create a file with the given name. + socketName.create(Ci.nsIFile.NORMAL_FILE_TYPE, allPermissions); + + // Try to create a socket with the same name. + do_check_throws_nsIException( + () => new UnixServerSocket(socketName, allPermissions, -1), + "NS_ERROR_SOCKET_ADDRESS_IN_USE" + ); + + // Try to create a socket under a name that uses that as a parent directory. + socketName.append("socket"); + do_check_throws_nsIException( + () => new UnixServerSocket(socketName, 0, -1), + "NS_ERROR_FILE_NOT_DIRECTORY" + ); + + run_next_test(); +} + +// It is not permitted to create a socket in a directory which we are not +// permitted to execute, or create files in. +function test_create_permission() { + let dirName = do_get_tempdir(); + dirName.append("unfriendly"); + + let socketName = dirName.clone(); + socketName.append("socket"); + + // The test harness has difficulty cleaning things up if we don't make + // everything writable before we're done. + try { + // Create a directory which we are not permitted to search. + dirName.create(Ci.nsIFile.DIRECTORY_TYPE, 0); + + // Try to create a socket in that directory. Because Linux returns EACCES + // when a 'connect' fails because of a local firewall rule, + // nsIServerSocket returns NS_ERROR_CONNECTION_REFUSED in this case. + do_check_throws_nsIException( + () => new UnixServerSocket(socketName, allPermissions, -1), + "NS_ERROR_CONNECTION_REFUSED" + ); + + // Grant read and execute permission, but not write permission on the directory. + dirName.permissions = parseInt("0555", 8); + + // This should also fail; we need write permission. + do_check_throws_nsIException( + () => new UnixServerSocket(socketName, allPermissions, -1), + "NS_ERROR_CONNECTION_REFUSED" + ); + } finally { + // Make the directory writable, so the test harness can clean it up. + dirName.permissions = allPermissions; + } + + // This should succeed, since we now have all the permissions on the + // directory we could want. + do_check_instanceof( + new UnixServerSocket(socketName, allPermissions, -1), + Ci.nsIServerSocket + ); + + run_next_test(); +} + +// To connect to a Unix domain socket, we need search permission on the +// directories containing it, and some kind of permission or other on the +// socket itself. +function test_connect_permission() { + // This test involves a lot of callbacks, but they're written out so that + // the actual control flow proceeds from top to bottom. + let log = ""; + + // Create a directory which we are permitted to search - at first. + let dirName = do_get_tempdir(); + dirName.append("inhospitable"); + dirName.create(Ci.nsIFile.DIRECTORY_TYPE, allPermissions); + + let socketName = dirName.clone(); + socketName.append("socket"); + + // Create a server socket in that directory, listening for connections, + // and accessible. + let server = new UnixServerSocket(socketName, allPermissions, -1); + server.asyncListen({ + onSocketAccepted: socketAccepted, + onStopListening: stopListening, + }); + + // Make the directory unsearchable. + dirName.permissions = 0; + + let client3; + + let client1 = socketTransportService.createUnixDomainTransport(socketName); + let client1AsyncInput = client1 + .openInputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncInputStream); + client1AsyncInput.asyncWait( + function (aStream) { + info("called test_connect_permission's client1's onInputStreamReady"); + log += "1"; + + // nsISocketTransport puts off actually creating sockets as long as + // possible, so the error doesn't actually show up until this point. + do_check_throws_nsIException( + () => client1AsyncInput.available(), + "NS_ERROR_CONNECTION_REFUSED" + ); + + client1AsyncInput.close(); + client1.close(Cr.NS_OK); + + // Make the directory searchable, but make the socket inaccessible. + dirName.permissions = allPermissions; + socketName.permissions = 0; + + let client2 = + socketTransportService.createUnixDomainTransport(socketName); + let client2AsyncInput = client2 + .openInputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncInputStream); + client2AsyncInput.asyncWait( + function (aStream) { + info("called test_connect_permission's client2's onInputStreamReady"); + log += "2"; + + do_check_throws_nsIException( + () => client2AsyncInput.available(), + "NS_ERROR_CONNECTION_REFUSED" + ); + + client2AsyncInput.close(); + client2.close(Cr.NS_OK); + + // Now make everything accessible, and try one last time. + socketName.permissions = allPermissions; + + client3 = + socketTransportService.createUnixDomainTransport(socketName); + + let client3Output = client3.openOutputStream(0, 0, 0); + client3Output.write("Hanratty", 8); + + let client3AsyncInput = client3 + .openInputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncInputStream); + client3AsyncInput.asyncWait( + client3InputStreamReady, + 0, + 0, + threadManager.currentThread + ); + }, + 0, + 0, + threadManager.currentThread + ); + }, + 0, + 0, + threadManager.currentThread + ); + + function socketAccepted(aServ, aTransport) { + info("called test_connect_permission's onSocketAccepted"); + log += "a"; + + let serverInput = aTransport + .openInputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncInputStream); + let serverOutput = aTransport.openOutputStream(0, 0, 0); + + serverInput.asyncWait( + function (aStream) { + info( + "called test_connect_permission's socketAccepted's onInputStreamReady" + ); + log += "i"; + + // Receive data from the client, and send back a response. + let serverScriptableInput = new ScriptableInputStream(serverInput); + Assert.equal(serverScriptableInput.readBytes(8), "Hanratty"); + serverOutput.write("Ferlingatti", 11); + }, + 0, + 0, + threadManager.currentThread + ); + } + + function client3InputStreamReady(aStream) { + info("called client3's onInputStreamReady"); + log += "3"; + + let client3Input = new ScriptableInputStream(aStream); + + Assert.equal(client3Input.readBytes(11), "Ferlingatti"); + + client3.close(Cr.NS_OK); + server.close(); + } + + function stopListening(aServ, aStatus) { + info("called test_connect_permission's server's stopListening"); + log += "s"; + + Assert.equal(log, "12ai3s"); + + run_next_test(); + } +} + +// Creating a socket with a long filename doesn't crash. +function test_long_socket_name() { + let socketName = do_get_tempdir(); + socketName.append(new Array(10000).join("long")); + + // Try to create a server socket with the long name. + do_check_throws_nsIException( + () => new UnixServerSocket(socketName, allPermissions, -1), + "NS_ERROR_FILE_NAME_TOO_LONG" + ); + + // Try to connect to a socket with the long name. + do_check_throws_nsIException( + () => socketTransportService.createUnixDomainTransport(socketName), + "NS_ERROR_FILE_NAME_TOO_LONG" + ); + + run_next_test(); +} + +// Going offline should not shut down Unix domain sockets. +function test_keep_when_offline() { + let log = ""; + + let socketName = do_get_tempdir(); + socketName.append("keep-when-offline"); + + // Create a listening socket. + let listener = new UnixServerSocket(socketName, allPermissions, -1); + listener.asyncListen({ onSocketAccepted: onAccepted, onStopListening }); + + // Connect a client socket to the listening socket. + let client = socketTransportService.createUnixDomainTransport(socketName); + let clientOutput = client.openOutputStream(0, 0, 0); + let clientInput = client.openInputStream(0, 0, 0); + clientInput.asyncWait(clientReady, 0, 0, threadManager.currentThread); + let clientScriptableInput = new ScriptableInputStream(clientInput); + + let server, serverInput, serverScriptableInput, serverOutput; + + // How many times has the server invited the client to go first? + let count = 0; + + // The server accepted connection callback. + function onAccepted(aListener, aServer) { + info("test_keep_when_offline: onAccepted called"); + log += "a"; + Assert.equal(aListener, listener); + server = aServer; + + // Prepare to receive messages from the client. + serverInput = server.openInputStream(0, 0, 0); + serverInput.asyncWait(serverReady, 0, 0, threadManager.currentThread); + serverScriptableInput = new ScriptableInputStream(serverInput); + + // Start a conversation with the client. + serverOutput = server.openOutputStream(0, 0, 0); + serverOutput.write("After you, Alphonse!", 20); + count++; + } + + // The client has seen its end of the socket close. + function clientReady(aStream) { + log += "c"; + info("test_keep_when_offline: clientReady called: " + log); + Assert.equal(aStream, clientInput); + + // If the connection has been closed, end the conversation and stop listening. + let available; + try { + available = clientInput.available(); + } catch (ex) { + do_check_instanceof(ex, Ci.nsIException); + Assert.equal(ex.result, Cr.NS_BASE_STREAM_CLOSED); + + info("client received end-of-stream; closing client output stream"); + log += ")"; + + client.close(Cr.NS_OK); + + // Now both output streams have been closed, and both input streams + // have received the close notification. Stop listening for + // connections. + listener.close(); + } + + if (available) { + // Check the message from the server. + Assert.equal(clientScriptableInput.readBytes(20), "After you, Alphonse!"); + + // Write our response to the server. + clientOutput.write("No, after you, Gaston!", 22); + + // Ask to be called again, when more input arrives. + clientInput.asyncWait(clientReady, 0, 0, threadManager.currentThread); + } + } + + function serverReady(aStream) { + log += "s"; + info("test_keep_when_offline: serverReady called: " + log); + Assert.equal(aStream, serverInput); + + // Check the message from the client. + Assert.equal(serverScriptableInput.readBytes(22), "No, after you, Gaston!"); + + // This should not shut things down: Unix domain sockets should + // remain open in offline mode. + if (count == 5) { + Services.io.offline = true; + log += "o"; + } + + if (count < 10) { + // Insist. + serverOutput.write("After you, Alphonse!", 20); + count++; + + // As long as the input stream is open, always ask to be called again + // when more input arrives. + serverInput.asyncWait(serverReady, 0, 0, threadManager.currentThread); + } else if (count == 10) { + // After sending ten times and receiving ten replies, we're not + // going to send any more. Close the server's output stream; the + // client's input stream should see this. + info("closing server transport"); + server.close(Cr.NS_OK); + log += "("; + } + } + + // We have stopped listening. + function onStopListening(aServ, aStatus) { + info("test_keep_when_offline: onStopListening called"); + log += "L"; + Assert.equal(log, "acscscscscsocscscscscs(c)L"); + + Assert.equal(aServ, listener); + Assert.equal(aStatus, Cr.NS_BINDING_ABORTED); + + run_next_test(); + } +} + +function test_abstract_address_socket() { + const socketname = "abstractsocket"; + let server = new UnixAbstractServerSocket(socketname, -1); + server.asyncListen({ + onSocketAccepted: (aServ, aTransport) => { + let serverInput = aTransport + .openInputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncInputStream); + let serverOutput = aTransport.openOutputStream(0, 0, 0); + + serverInput.asyncWait( + aStream => { + info( + "called test_abstract_address_socket's onSocketAccepted's onInputStreamReady" + ); + + // Receive data from the client, and send back a response. + let serverScriptableInput = new ScriptableInputStream(serverInput); + Assert.equal(serverScriptableInput.readBytes(9), "ping ping"); + serverOutput.write("pong", 4); + }, + 0, + 0, + threadManager.currentThread + ); + }, + onStopListening: (aServ, aTransport) => {}, + }); + + let client = + socketTransportService.createUnixDomainAbstractAddressTransport(socketname); + Assert.equal(client.host, socketname); + Assert.equal(client.port, 0); + let clientInput = client + .openInputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncInputStream); + let clientOutput = client.openOutputStream(0, 0, 0); + + clientOutput.write("ping ping", 9); + + clientInput.asyncWait( + aStream => { + let clientScriptInput = new ScriptableInputStream(clientInput); + let available = clientScriptInput.available(); + if (available) { + Assert.equal(clientScriptInput.readBytes(4), "pong"); + + client.close(Cr.NS_OK); + server.close(Cr.NS_OK); + + run_next_test(); + } + }, + 0, + 0, + threadManager.currentThread + ); +} diff --git a/netwerk/test/unit/test_uri_mutator.js b/netwerk/test/unit/test_uri_mutator.js new file mode 100644 index 0000000000..fb9228fc5b --- /dev/null +++ b/netwerk/test/unit/test_uri_mutator.js @@ -0,0 +1,48 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 standardMutator() { + return Cc["@mozilla.org/network/standard-url-mutator;1"].createInstance( + Ci.nsIURIMutator + ); +} + +add_task(async function test_simple_setter_chaining() { + let uri = standardMutator() + .setSpec("http://example.com/") + .setQuery("hello") + .setRef("bla") + .setScheme("ftp") + .finalize(); + equal(uri.spec, "ftp://example.com/?hello#bla"); +}); + +add_task(async function test_qi_behaviour() { + let uri = standardMutator() + .setSpec("http://example.com/") + .QueryInterface(Ci.nsIURI); + equal(uri.spec, "http://example.com/"); + + Assert.throws( + () => { + uri = standardMutator().QueryInterface(Ci.nsIURI); + }, + /NS_NOINTERFACE/, + "mutator doesn't QI if it holds no URI" + ); + + let mutator = standardMutator().setSpec("http://example.com/path"); + uri = mutator.QueryInterface(Ci.nsIURI); + equal(uri.spec, "http://example.com/path"); + Assert.throws( + () => { + uri = mutator.QueryInterface(Ci.nsIURI); + }, + /NS_NOINTERFACE/, + "Second QueryInterface should fail" + ); +}); diff --git a/netwerk/test/unit/test_use_httpssvc.js b/netwerk/test/unit/test_use_httpssvc.js new file mode 100644 index 0000000000..1085ca0959 --- /dev/null +++ b/netwerk/test/unit/test_use_httpssvc.js @@ -0,0 +1,240 @@ +/* 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"; + +let h2Port; + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +add_setup(async function setup() { + trr_test_setup(); + + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + + registerCleanupFunction(() => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + }); + + if (mozinfo.socketprocess_networking) { + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } + + Services.prefs.setIntPref("network.trr.mode", 2); // TRR first +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + chan.asyncOpen(new ChannelListener(finish, null, CL_ALLOW_UNKNOWN_CL)); + }); +} + +// This is for testing when the HTTPSSVC record is not available when +// the transaction is added in connection manager. +add_task(async function testUseHTTPSSVCForHttpsUpgrade() { + // use the h2 server as DOH provider + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc" + ); + Services.dns.clearCache(true); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + let chan = makeChan(`https://test.httpssvc.com:8080/`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); +}); + +class EventSinkListener { + getInterface(iid) { + if (iid.equals(Ci.nsIChannelEventSink)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } + asyncOnChannelRedirect(oldChan, newChan, flags, callback) { + Assert.equal(oldChan.URI.hostPort, newChan.URI.hostPort); + Assert.equal(oldChan.URI.scheme, "http"); + Assert.equal(newChan.URI.scheme, "https"); + callback.onRedirectVerifyCallback(Cr.NS_OK); + } +} + +EventSinkListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIChannelEventSink", +]); + +// Test if the request is upgraded to https with a HTTPSSVC record. +add_task(async function testUseHTTPSSVCAsHSTS() { + // use the h2 server as DOH provider + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc" + ); + Services.dns.clearCache(true); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + // At this time, the DataStorage is not ready, so MaybeUseHTTPSRRForUpgrade() + // is called from the callback of NS_ShouldSecureUpgrade(). + let chan = makeChan(`http://test.httpssvc.com:80/`); + let listener = new EventSinkListener(); + chan.notificationCallbacks = listener; + + let [req] = await channelOpenPromise(chan); + + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + // At this time, the DataStorage is ready, so MaybeUseHTTPSRRForUpgrade() + // is called from nsHttpChannel::OnBeforeConnect(). + chan = makeChan(`http://test.httpssvc.com:80/`); + listener = new EventSinkListener(); + chan.notificationCallbacks = listener; + + [req] = await channelOpenPromise(chan); + + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); +}); + +// This is for testing when the HTTPSSVC record is already available before +// the transaction is added in connection manager. +add_task(async function testUseHTTPSSVC() { + // use the h2 server as DOH provider + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc" + ); + + // Do DNS resolution before creating the channel, so the HTTPSSVC record will + // be resolved from the cache. + await new TRRDNSListener("test.httpssvc.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + // We need to skip the security check, since our test cert is signed for + // foo.example.com, not test.httpssvc.com. + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + let chan = makeChan(`https://test.httpssvc.com:8888`); + let [req] = await channelOpenPromise(chan); + // Test if this request is done by h2. + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); +}); + +// Test if we can successfully fallback to the original host and port. +add_task(async function testFallback() { + let trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.fallback.com", "A", { + answers: [ + { + name: "test.fallback.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + // Use a wrong port number 8888, so this connection will be refused. + await trrServer.registerDoHAnswers("test.fallback.com", "HTTPS", { + answers: [ + { + name: "test.fallback.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "foo.example.com", + values: [{ key: "port", value: 8888 }], + }, + }, + ], + }); + + let { inRecord } = await new TRRDNSListener("test.fallback.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + + let record = inRecord + .QueryInterface(Ci.nsIDNSHTTPSSVCRecord) + .GetServiceModeRecord(false, false); + Assert.equal(record.priority, 1); + Assert.equal(record.name, "foo.example.com"); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + // When the connection with port 8888 failed, the correct h2Port will be used + // to connect again. + let chan = makeChan(`https://test.fallback.com:${h2Port}`); + let [req] = await channelOpenPromise(chan); + // Test if this request is done by h2. + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); +}); diff --git a/netwerk/test/unit/test_websocket_500k.js b/netwerk/test/unit/test_websocket_500k.js new file mode 100644 index 0000000000..f974283437 --- /dev/null +++ b/netwerk/test/unit/test_websocket_500k.js @@ -0,0 +1,222 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ + +XPCOMUtils.defineLazyModuleGetters(this, { + ObjectUtils: "resource://gre/modules/ObjectUtils.jsm", +}); + +add_setup(async function () { + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); +}); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.dns.localDomains"); +}); + +async function channelOpenPromise(url, msg) { + let conn = new WebSocketConnection(); + await conn.open(url); + conn.send(msg); + let res = await conn.receiveMessages(); + conn.close(); + let { status } = await conn.finished(); + return [status, res]; +} + +async function sendDataAndCheck(url) { + let data = "a".repeat(500000); + let [status, res] = await channelOpenPromise(url, data); + Assert.equal(status, Cr.NS_OK); + // Use "ObjectUtils.deepEqual" directly to avoid printing data. + Assert.ok(ObjectUtils.deepEqual(res, [data])); +} + +add_task(async function test_h2_websocket_500k() { + Services.prefs.setBoolPref("network.http.http2.websockets", true); + let wss = new NodeWebSocketHttp2Server(); + await wss.start(); + registerCleanupFunction(async () => wss.stop()); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + let url = `wss://foo.example.com:${wss.port()}`; + await sendDataAndCheck(url); +}); + +// h1.1 direct +add_task(async function test_h1_websocket_direct() { + let wss = new NodeWebSocketServer(); + await wss.start(); + registerCleanupFunction(async () => wss.stop()); + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + let url = `wss://localhost:${wss.port()}`; + await sendDataAndCheck(url); +}); + +// ws h1.1 with insecure h1.1 proxy +add_task(async function test_h1_ws_with_h1_insecure_proxy() { + Services.prefs.setBoolPref("network.http.http2.websockets", false); + let proxy = new NodeHTTPProxyServer(); + await proxy.start(); + + let wss = new NodeWebSocketServer(); + await wss.start(); + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + Assert.notEqual(wss.port(), null); + + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + let url = `wss://localhost:${wss.port()}`; + await sendDataAndCheck(url); +}); + +// h1 server with secure h1.1 proxy +add_task(async function test_h1_ws_with_secure_h1_proxy() { + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + + let wss = new NodeWebSocketServer(); + await wss.start(); + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + + let url = `wss://localhost:${wss.port()}`; + await sendDataAndCheck(url); + + await proxy.stop(); +}); + +// ws h1.1 with h2 proxy +add_task(async function test_h1_ws_with_h2_proxy() { + Services.prefs.setBoolPref("network.http.http2.websockets", false); + + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); + + let wss = new NodeWebSocketServer(); + await wss.start(); + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + + let url = `wss://localhost:${wss.port()}`; + await sendDataAndCheck(url); + + await proxy.stop(); +}); + +// ws h2 with insecure h1.1 proxy +add_task(async function test_h2_ws_with_h1_insecure_proxy() { + Services.prefs.setBoolPref("network.http.http2.websockets", true); + + let proxy = new NodeHTTPProxyServer(); + await proxy.start(); + + let wss = new NodeWebSocketHttp2Server(); + await wss.start(); + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + + let url = `wss://localhost:${wss.port()}`; + await sendDataAndCheck(url); + + await proxy.stop(); +}); + +add_task(async function test_h2_ws_with_h1_secure_proxy() { + Services.prefs.setBoolPref("network.http.http2.websockets", true); + + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + + let wss = new NodeWebSocketHttp2Server(); + await wss.start(); + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + + let url = `wss://localhost:${wss.port()}`; + await sendDataAndCheck(url); + + await proxy.stop(); +}); + +// ws h2 with secure h2 proxy +add_task(async function test_h2_ws_with_h2_proxy() { + Services.prefs.setBoolPref("network.http.http2.websockets", true); + + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); // start and register proxy "filter" + + let wss = new NodeWebSocketHttp2Server(); + await wss.start(); // init port + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + + let url = `wss://localhost:${wss.port()}`; + await sendDataAndCheck(url); + + await proxy.stop(); +}); diff --git a/netwerk/test/unit/test_websocket_fails.js b/netwerk/test/unit/test_websocket_fails.js new file mode 100644 index 0000000000..9acb1bfcd2 --- /dev/null +++ b/netwerk/test/unit/test_websocket_fails.js @@ -0,0 +1,194 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ +/* import-globals-from head_websocket.js */ + +var CC = Components.Constructor; +const ServerSocket = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init" +); + +let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB +); + +add_setup(() => { + Services.prefs.setBoolPref("network.http.http2.websockets", true); +}); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.http.http2.websockets"); +}); + +// TLS handshake to the end server fails - no proxy +async function test_tls_fail_on_direct_ws_server_handshake() { + // no cert and no proxy + let wss = new NodeWebSocketServer(); + await wss.start(); + registerCleanupFunction(async () => { + await wss.stop(); + }); + + Assert.notEqual(wss.port(), null); + + let chan = makeWebSocketChan(); + let url = `wss://localhost:${wss.port()}`; + const msg = "test tls handshake with direct ws server fails"; + let [status] = await openWebSocketChannelPromise(chan, url, msg); + + // can be two errors, seems to be a race between: + // * overwriting the WebSocketChannel status with NS_ERROR_NET_RESET and + // * getting the original 805A1FF3 // SEC_ERROR_UNKNOWN_ISSUER + if (status == 2152398930) { + Assert.equal(status, 0x804b0052); // NS_ERROR_NET_INADEQUATE_SECURITY + } else { + // occasionally this happens + Assert.equal(status, 0x804b0057); // NS_ERROR_WEBSOCKET_CONNECTION_REFUSED + } +} + +// TLS handshake to proxy fails +async function test_tls_fail_on_proxy_handshake() { + // we have ws cert, but no proxy cert + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + + let wss = new NodeWebSocketServer(); + await wss.start(); + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + Assert.notEqual(wss.port(), null); + + let chan = makeWebSocketChan(); + let url = `wss://localhost:${wss.port()}`; + const msg = "test tls failure on proxy handshake"; + let [status] = await openWebSocketChannelPromise(chan, url, msg); + + // see above test for details on why 2 cases here + if (status == 2152398930) { + Assert.equal(status, 0x804b0052); // NS_ERROR_NET_INADEQUATE_SECURITY + } else { + Assert.equal(status, 0x804b0057); // NS_ERROR_WEBSOCKET_CONNECTION_REFUSED + } + + await proxy.stop(); +} + +// the ws server does not respond (closed port) +async function test_non_responsive_ws_server_closed_port() { + // ws server cert already added in previous test + + // no ws server listening (closed port) + let randomPort = 666; // "random" port + let chan = makeWebSocketChan(); + let url = `wss://localhost:${randomPort}`; + const msg = "test non-responsive ws server closed port"; + let [status] = await openWebSocketChannelPromise(chan, url, msg); + Assert.equal(status, 0x804b0057); // NS_ERROR_WEBSOCKET_CONNECTION_REFUSED +} + +// no ws response from server (ie. no ws server, use tcp server to open port) +async function test_non_responsive_ws_server_open_port() { + // we are expecting the timeout in this test, so lets shorten to 1s + Services.prefs.setIntPref("network.websocket.timeout.open", 1); + + // ws server cert already added in previous test + + // use a tcp server to test open port, not a ws server + var server = ServerSocket(-1, true, -1); // port, loopback, default-backlog + var port = server.port; + info("server: listening on " + server.port); + server.asyncListen({}); + + // queue cleanup after all tests + registerCleanupFunction(() => { + server.close(); + Services.prefs.clearUserPref("network.websocket.timeout.open"); + }); + + // try ws connection + let chan = makeWebSocketChan(); + let url = `wss://localhost:${port}`; + const msg = "test non-responsive ws server open port"; + let [status] = await openWebSocketChannelPromise(chan, url, msg); + Assert.equal(status, Cr.NS_ERROR_NET_TIMEOUT_EXTERNAL); // we will timeout + Services.prefs.clearUserPref("network.websocket.timeout.open"); +} + +// proxy does not respond +async function test_proxy_doesnt_respond() { + Services.prefs.setIntPref("network.websocket.timeout.open", 1); + Services.prefs.setBoolPref("network.http.http2.websockets", false); + // ws cert added in previous test, add proxy cert + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + info("spinning up proxy"); + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + + // route traffic through non-existant proxy + const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + let randomPort = proxy.port() + 1; + var filter = new NodeProxyFilter( + proxy.protocol(), + "localhost", + randomPort, + 0 + ); + pps.registerFilter(filter, 10); + + registerCleanupFunction(async () => { + await proxy.stop(); + Services.prefs.clearUserPref("network.websocket.timeout.open"); + }); + + // setup the websocket server + info("spinning up websocket server"); + let wss = new NodeWebSocketServer(); + await wss.start(); + registerCleanupFunction(() => { + wss.stop(); + }); + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + + info("creating and connecting websocket"); + let url = `wss://localhost:${wss.port()}`; + let conn = new WebSocketConnection(); + conn.open(url); // do not await, we don't expect a fully opened channel + + // check proxy info + info("checking proxy info"); + let proxyInfoPromise = conn.getProxyInfo(); + let proxyInfo = await proxyInfoPromise; + Assert.equal(proxyInfo.type, "https"); // let's be sure that failure is not "direct" + + // we fail to connect via proxy, as expected + let { status } = await conn.finished(); + info("stats: " + status); + Assert.equal(status, 0x804b0057); // NS_ERROR_WEBSOCKET_CONNECTION_REFUSED +} + +add_task(test_tls_fail_on_direct_ws_server_handshake); +add_task(test_tls_fail_on_proxy_handshake); +add_task(test_non_responsive_ws_server_closed_port); +add_task(test_non_responsive_ws_server_open_port); +add_task(test_proxy_doesnt_respond); diff --git a/netwerk/test/unit/test_websocket_fails_2.js b/netwerk/test/unit/test_websocket_fails_2.js new file mode 100644 index 0000000000..57c9b321ad --- /dev/null +++ b/netwerk/test/unit/test_websocket_fails_2.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/. */ + +"use strict"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ +/* import-globals-from head_websocket.js */ + +let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB +); + +add_setup(() => { + Services.prefs.setBoolPref("network.http.http2.websockets", true); +}); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.http.http2.websockets"); +}); + +// TLS handshake to the end server fails with proxy +async function test_tls_fail_on_ws_server_over_proxy() { + // we are expecting a timeout, so lets shorten how long we must wait + Services.prefs.setIntPref("network.websocket.timeout.open", 1); + + // no cert to ws server + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + + let wss = new NodeWebSocketServer(); + await wss.start(); + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + Services.prefs.clearUserPref("network.websocket.timeout.open"); + }); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + + let chan = makeWebSocketChan(); + let url = `wss://localhost:${wss.port()}`; + const msg = "test tls fail on ws server over proxy"; + let [status] = await openWebSocketChannelPromise(chan, url, msg); + + Assert.equal(status, Cr.NS_ERROR_NET_TIMEOUT_EXTERNAL); +} +add_task(test_tls_fail_on_ws_server_over_proxy); diff --git a/netwerk/test/unit/test_websocket_offline.js b/netwerk/test/unit/test_websocket_offline.js new file mode 100644 index 0000000000..1f13879dbc --- /dev/null +++ b/netwerk/test/unit/test_websocket_offline.js @@ -0,0 +1,51 @@ +/* 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"; + +// checking to make sure we don't hang as per 1038304 +// offline so url isn't impt +var url = "ws://localhost"; +var chan; +var offlineStatus; + +var listener = { + onAcknowledge(aContext, aSize) {}, + onBinaryMessageAvailable(aContext, aMsg) {}, + onMessageAvailable(aContext, aMsg) {}, + onServerClose(aContext, aCode, aReason) {}, + onStart(aContext) { + // onStart is not called when a connection fails + Assert.ok(false); + }, + onStop(aContext, aStatusCode) { + Assert.notEqual(aStatusCode, Cr.NS_OK); + Services.io.offline = offlineStatus; + do_test_finished(); + }, +}; + +function run_test() { + offlineStatus = Services.io.offline; + Services.io.offline = true; + + try { + chan = Cc["@mozilla.org/network/protocol;1?name=ws"].createInstance( + Ci.nsIWebSocketChannel + ); + chan.initLoadInfo( + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_WEBSOCKET + ); + + var uri = Services.io.newURI(url); + chan.asyncOpen(uri, url, {}, 0, listener, null); + do_test_pending(); + } catch (x) { + dump("throwing " + x); + do_throw(x); + } +} diff --git a/netwerk/test/unit/test_websocket_server.js b/netwerk/test/unit/test_websocket_server.js new file mode 100644 index 0000000000..8d760cb65a --- /dev/null +++ b/netwerk/test/unit/test_websocket_server.js @@ -0,0 +1,267 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +add_setup(async function setup() { + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + registerCleanupFunction(async () => { + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + }); +}); + +async function channelOpenPromise(url, msg) { + let conn = new WebSocketConnection(); + await conn.open(url); + conn.send(msg); + let res = await conn.receiveMessages(); + conn.close(); + let { status } = await conn.finished(); + return [status, res]; +} + +// h1.1 direct +async function test_h1_websocket_direct() { + let wss = new NodeWebSocketServer(); + await wss.start(); + registerCleanupFunction(async () => wss.stop()); + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + let url = `wss://localhost:${wss.port()}`; + const msg = "test websocket"; + + let conn = new WebSocketConnection(); + await conn.open(url); + conn.send(msg); + let mess1 = await conn.receiveMessages(); + Assert.deepEqual(mess1, [msg]); + + // Now send 3 more, and check that we received all of them + conn.send(msg); + conn.send(msg); + conn.send(msg); + let mess2 = []; + while (mess2.length < 3) { + // receive could return 1, 2 or all 3 replies. + mess2 = mess2.concat(await conn.receiveMessages()); + } + Assert.deepEqual(mess2, [msg, msg, msg]); + + conn.close(); + let { status } = await conn.finished(); + + Assert.equal(status, Cr.NS_OK); +} + +// h1 server with secure h1.1 proxy +async function test_h1_ws_with_secure_h1_proxy() { + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + + let wss = new NodeWebSocketServer(); + await wss.start(); + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + + let url = `wss://localhost:${wss.port()}`; + const msg = "test h1.1 websocket with h1.1 secure proxy"; + let [status, res] = await channelOpenPromise(url, msg); + Assert.equal(status, Cr.NS_OK); + Assert.deepEqual(res, [msg]); + + await proxy.stop(); +} + +async function test_h2_websocket_direct() { + Services.prefs.setBoolPref("network.http.http2.websockets", true); + let wss = new NodeWebSocketHttp2Server(); + await wss.start(); + registerCleanupFunction(async () => wss.stop()); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + let url = `wss://localhost:${wss.port()}`; + const msg = "test h2 websocket h2 direct"; + let [status, res] = await channelOpenPromise(url, msg); + Assert.equal(status, Cr.NS_OK); + Assert.deepEqual(res, [msg]); +} + +// ws h1.1 with insecure h1.1 proxy +async function test_h1_ws_with_h1_insecure_proxy() { + Services.prefs.setBoolPref("network.http.http2.websockets", false); + let proxy = new NodeHTTPProxyServer(); + await proxy.start(); + + let wss = new NodeWebSocketServer(); + await wss.start(); + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + Assert.notEqual(wss.port(), null); + + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + let url = `wss://localhost:${wss.port()}`; + const msg = "test h1 websocket with h1 insecure proxy"; + let [status, res] = await channelOpenPromise(url, msg); + Assert.equal(status, Cr.NS_OK); + Assert.deepEqual(res, [msg]); + + await proxy.stop(); +} + +// ws h1.1 with h2 proxy +async function test_h1_ws_with_h2_proxy() { + Services.prefs.setBoolPref("network.http.http2.websockets", false); + + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); + + let wss = new NodeWebSocketServer(); + await wss.start(); + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + + let url = `wss://localhost:${wss.port()}`; + const msg = "test h1 websocket with h2 proxy"; + let [status, res] = await channelOpenPromise(url, msg); + Assert.equal(status, Cr.NS_OK); + Assert.deepEqual(res, [msg]); + + await proxy.stop(); +} + +// ws h2 with insecure h1.1 proxy +async function test_h2_ws_with_h1_insecure_proxy() { + Services.prefs.setBoolPref("network.http.http2.websockets", true); + + let proxy = new NodeHTTPProxyServer(); + await proxy.start(); + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + let wss = new NodeWebSocketHttp2Server(); + await wss.start(); + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + + let url = `wss://localhost:${wss.port()}`; + const msg = "test h2 websocket with h1 insecure proxy"; + let [status, res] = await channelOpenPromise(url, msg); + Assert.equal(status, Cr.NS_OK); + Assert.deepEqual(res, [msg]); + + await proxy.stop(); +} + +// ws h2 with secure h1 proxy +async function test_h2_ws_with_h1_secure_proxy() { + Services.prefs.setBoolPref("network.http.http2.websockets", true); + + let proxy = new NodeHTTPSProxyServer(); + await proxy.start(); + + let wss = new NodeWebSocketHttp2Server(); + await wss.start(); + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + + let url = `wss://localhost:${wss.port()}`; + const msg = "test h2 websocket with h1 secure proxy"; + let [status, res] = await channelOpenPromise(url, msg); + Assert.equal(status, Cr.NS_OK); + Assert.deepEqual(res, [msg]); + + await proxy.stop(); +} + +// ws h2 with secure h2 proxy +async function test_h2_ws_with_h2_proxy() { + Services.prefs.setBoolPref("network.http.http2.websockets", true); + + let proxy = new NodeHTTP2ProxyServer(); + await proxy.start(); // start and register proxy "filter" + + let wss = new NodeWebSocketHttp2Server(); + await wss.start(); // init port + + registerCleanupFunction(async () => { + await wss.stop(); + await proxy.stop(); + }); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + + let url = `wss://localhost:${wss.port()}`; + const msg = "test h2 websocket with h2 proxy"; + let [status, res] = await channelOpenPromise(url, msg); + Assert.equal(status, Cr.NS_OK); + Assert.deepEqual(res, [msg]); + + await proxy.stop(); +} + +add_task(test_h1_websocket_direct); +add_task(test_h2_websocket_direct); +add_task(test_h1_ws_with_secure_h1_proxy); +add_task(test_h1_ws_with_h1_insecure_proxy); +add_task(test_h1_ws_with_h2_proxy); +add_task(test_h2_ws_with_h1_insecure_proxy); +add_task(test_h2_ws_with_h1_secure_proxy); +add_task(test_h2_ws_with_h2_proxy); diff --git a/netwerk/test/unit/test_websocket_server_multiclient.js b/netwerk/test/unit/test_websocket_server_multiclient.js new file mode 100644 index 0000000000..91c5b5d55f --- /dev/null +++ b/netwerk/test/unit/test_websocket_server_multiclient.js @@ -0,0 +1,144 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ +/* import-globals-from head_websocket.js */ + +// These test should basically match the ones in test_websocket_server.js, +// but with multiple websocket clients making requests on the same server + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +// setup +add_setup(async function setup() { + // turn off cert checking for these tests + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); +}); + +// append cleanup to cleanup queue +registerCleanupFunction(async () => { + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + Services.prefs.clearUserPref("network.http.http2.websockets"); +}); + +async function spinup_and_check(proxy_kind, ws_kind) { + let ws_h2 = true; + if (ws_kind == NodeWebSocketServer) { + info("not h2 ws"); + ws_h2 = false; + } + Services.prefs.setBoolPref("network.http.http2.websockets", ws_h2); + + let proxy; + if (proxy_kind) { + proxy = new proxy_kind(); + await proxy.start(); + registerCleanupFunction(async () => proxy.stop()); + } + + let wss = new ws_kind(); + await wss.start(); + registerCleanupFunction(async () => wss.stop()); + + Assert.notEqual(wss.port(), null); + await wss.registerMessageHandler((data, ws) => { + ws.send(data); + }); + let url = `wss://localhost:${wss.port()}`; + + let conn1 = new WebSocketConnection(); + await conn1.open(url); + + let conn2 = new WebSocketConnection(); + await conn2.open(url); + + conn1.send("msg1"); + conn2.send("msg2"); + + let mess2 = await conn2.receiveMessages(); + Assert.deepEqual(mess2, ["msg2"]); + + conn1.send("msg1 again"); + let mess1 = []; + while (mess1.length < 2) { + // receive could return only the first or both replies. + mess1 = mess1.concat(await conn1.receiveMessages()); + } + Assert.deepEqual(mess1, ["msg1", "msg1 again"]); + + conn1.close(); + conn2.close(); + Assert.deepEqual({ status: Cr.NS_OK }, await conn1.finished()); + Assert.deepEqual({ status: Cr.NS_OK }, await conn2.finished()); + await wss.stop(); + + if (proxy_kind) { + await proxy.stop(); + } +} + +// h1.1 direct +async function test_h1_websocket_direct() { + await spinup_and_check(null, NodeWebSocketServer); +} + +// h2 direct +async function test_h2_websocket_direct() { + await spinup_and_check(null, NodeWebSocketHttp2Server); +} + +// ws h1.1 with secure h1.1 proxy +async function test_h1_ws_with_secure_h1_proxy() { + await spinup_and_check(NodeHTTPSProxyServer, NodeWebSocketServer); +} + +// ws h1.1 with insecure h1.1 proxy +async function test_h1_ws_with_insecure_h1_proxy() { + await spinup_and_check(NodeHTTPProxyServer, NodeWebSocketServer); +} + +// ws h1.1 with h2 proxy +async function test_h1_ws_with_h2_proxy() { + await spinup_and_check(NodeHTTP2ProxyServer, NodeWebSocketServer); +} + +// ws h2 with insecure h1.1 proxy +async function test_h2_ws_with_insecure_h1_proxy() { + // disabled until bug 1800533 complete + // await spinup_and_check(NodeHTTPProxyServer, NodeWebSocketHttp2Server); +} + +// ws h2 with secure h1 proxy +async function test_h2_ws_with_secure_h1_proxy() { + // disabled until bug 1800533 complete + // await spinup_and_check(NodeHTTPSProxyServer, NodeWebSocketHttp2Server); +} + +// ws h2 with secure h2 proxy +async function test_h2_ws_with_h2_proxy() { + // disabled until bug 1800533 complete + // await spinup_and_check(NodeHTTP2ProxyServer, NodeWebSocketHttp2Server); +} + +add_task(test_h1_websocket_direct); +add_task(test_h2_websocket_direct); +add_task(test_h1_ws_with_secure_h1_proxy); +add_task(test_h1_ws_with_insecure_h1_proxy); +add_task(test_h1_ws_with_h2_proxy); + +// any multi-client test with h2 websocket and any kind of proxy will fail/hang +add_task(test_h2_ws_with_insecure_h1_proxy); +add_task(test_h2_ws_with_secure_h1_proxy); +add_task(test_h2_ws_with_h2_proxy); diff --git a/netwerk/test/unit/test_websocket_with_h3_active.js b/netwerk/test/unit/test_websocket_with_h3_active.js new file mode 100644 index 0000000000..f9ed2b08a0 --- /dev/null +++ b/netwerk/test/unit/test_websocket_with_h3_active.js @@ -0,0 +1,97 @@ +/* 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"; + +registerCleanupFunction(async () => { + http3_clear_prefs(); +}); + +let wssUri; +let httpsUri; + +add_task(async function pre_setup() { + let h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + wssUri = "wss://foo.example.com:" + h2Port + "/websocket"; + httpsUri = "https://foo.example.com:" + h2Port + "/"; + Services.prefs.setBoolPref("network.http.http3.support_version1", true); +}); + +add_task(async function setup() { + await http3_setup_tests("h3"); +}); + +WebSocketListener.prototype = { + onAcknowledge(aContext, aSize) {}, + onBinaryMessageAvailable(aContext, aMsg) {}, + onMessageAvailable(aContext, aMsg) {}, + onServerClose(aContext, aCode, aReason) {}, + onStart(aContext) { + this.finish(); + }, + onStop(aContext, aStatusCode) {}, +}; + +function makeH2Chan() { + let chan = NetUtil.newChannel({ + uri: httpsUri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +add_task(async function open_wss_when_h3_is_active() { + // Make an active connection using HTTP/3 + let chanHttp1 = makeH2Chan(httpsUri); + await new Promise(resolve => { + chanHttp1.asyncOpen( + new ChannelListener(request => { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + resolve(); + }) + ); + }); + + // Now try to connect ot a WebSocket on the same port -> this should not loop + // see bug 1717360. + let chan = Cc["@mozilla.org/network/protocol;1?name=wss"].createInstance( + Ci.nsIWebSocketChannel + ); + chan.initLoadInfo( + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_WEBSOCKET + ); + + var uri = Services.io.newURI(wssUri); + var wsListener = new WebSocketListener(); + await new Promise(resolve => { + wsListener.finish = resolve; + chan.asyncOpen(uri, wssUri, {}, 0, wsListener, null); + }); + + // Try to use https protocol, it should sttill use HTTP/3 + let chanHttp2 = makeH2Chan(httpsUri); + await new Promise(resolve => { + chanHttp2.asyncOpen( + new ChannelListener(request => { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + resolve(); + }) + ); + }); +}); diff --git a/netwerk/test/unit/test_webtransport_simple.js b/netwerk/test/unit/test_webtransport_simple.js new file mode 100644 index 0000000000..dbd788d278 --- /dev/null +++ b/netwerk/test/unit/test_webtransport_simple.js @@ -0,0 +1,390 @@ +// +// Simple WebTransport test +// + +/* import-globals-from head_webtransport.js */ + +"use strict"; + +var h3Port; +var host; + +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.webtransport.datagrams.enabled"); +}); + +add_task(async function setup() { + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + Services.prefs.setBoolPref("network.webtransport.datagrams.enabled", true); + + h3Port = Services.env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + host = "foo.example.com:" + h3Port; + do_get_profile(); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + // `../unit/` so that unit_ipc tests can use as well + addCertFromFile(certdb, "../unit/http2-ca.pem", "CTu,u,u"); +}); + +add_task(async function test_wt_datagram() { + let webTransport = NetUtil.newWebTransport(); + let listener = new WebTransportListener().QueryInterface( + Ci.WebTransportSessionEventListener + ); + + let pReady = new Promise(resolve => { + listener.ready = resolve; + }); + let pData = new Promise(resolve => { + listener.onDatagram = resolve; + }); + let pSize = new Promise(resolve => { + listener.onMaxDatagramSize = resolve; + }); + let pOutcome = new Promise(resolve => { + listener.onDatagramOutcome = resolve; + }); + + webTransport.asyncConnect( + NetUtil.newURI(`https://${host}/success`), + Services.scriptSecurityManager.getSystemPrincipal(), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + listener + ); + + await pReady; + + webTransport.getMaxDatagramSize(); + let size = await pSize; + info("max size:" + size); + + let rawData = new Uint8Array(size); + rawData.fill(42); + + webTransport.sendDatagram(rawData, 1); + let { id, outcome } = await pOutcome; + Assert.equal(id, 1); + Assert.equal(outcome, Ci.WebTransportSessionEventListener.SENT); + + let received = await pData; + Assert.deepEqual(received, rawData); + + webTransport.getMaxDatagramSize(); + size = await pSize; + info("max size:" + size); + + rawData = new Uint8Array(size + 1); + webTransport.sendDatagram(rawData, 2); + + pOutcome = new Promise(resolve => { + listener.onDatagramOutcome = resolve; + }); + ({ id, outcome } = await pOutcome); + Assert.equal(id, 2); + Assert.equal( + outcome, + Ci.WebTransportSessionEventListener.DROPPED_TOO_MUCH_DATA + ); + + webTransport.closeSession(0, ""); +}); + +add_task(async function test_connect_wt() { + let webTransport = NetUtil.newWebTransport(); + + await new Promise(resolve => { + let listener = new WebTransportListener().QueryInterface( + Ci.WebTransportSessionEventListener + ); + listener.ready = resolve; + + webTransport.asyncConnect( + NetUtil.newURI(`https://${host}/success`), + Services.scriptSecurityManager.getSystemPrincipal(), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + listener + ); + }); + + webTransport.closeSession(0, ""); +}); + +add_task(async function test_redirect_wt() { + let webTransport = NetUtil.newWebTransport(); + + await new Promise(resolve => { + let listener = new WebTransportListener().QueryInterface( + Ci.WebTransportSessionEventListener + ); + + listener.closed = resolve; + + webTransport.asyncConnect( + NetUtil.newURI(`https://${host}/redirect`), + Services.scriptSecurityManager.getSystemPrincipal(), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + listener + ); + }); +}); + +add_task(async function test_reject() { + let webTransport = NetUtil.newWebTransport(); + + await new Promise(resolve => { + let listener = new WebTransportListener().QueryInterface( + Ci.WebTransportSessionEventListener + ); + listener.closed = resolve; + + webTransport.asyncConnect( + NetUtil.newURI(`https://${host}/reject`), + Services.scriptSecurityManager.getSystemPrincipal(), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + listener + ); + }); +}); + +async function test_closed(path) { + let webTransport = NetUtil.newWebTransport(); + + let listener = new WebTransportListener().QueryInterface( + Ci.WebTransportSessionEventListener + ); + + let pReady = new Promise(resolve => { + listener.ready = resolve; + }); + let pClose = new Promise(resolve => { + listener.closed = resolve; + }); + webTransport.asyncConnect( + NetUtil.newURI(`https://${host}${path}`), + Services.scriptSecurityManager.getSystemPrincipal(), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + listener + ); + + await pReady; + await pClose; +} + +add_task(async function test_closed_0ms() { + await test_closed("/closeafter0ms"); +}); + +add_task(async function test_closed_100ms() { + await test_closed("/closeafter100ms"); +}); + +add_task(async function test_wt_stream_create() { + let webTransport = NetUtil.newWebTransport().QueryInterface( + Ci.nsIWebTransport + ); + + await new Promise(resolve => { + let listener = new WebTransportListener().QueryInterface( + Ci.WebTransportSessionEventListener + ); + listener.ready = resolve; + + webTransport.asyncConnect( + NetUtil.newURI(`https://${host}/success`), + Services.scriptSecurityManager.getSystemPrincipal(), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + listener + ); + }); + + await streamCreatePromise(webTransport, true); + await streamCreatePromise(webTransport, false); + + webTransport.closeSession(0, ""); +}); + +add_task(async function test_wt_stream_send_and_stats() { + let webTransport = NetUtil.newWebTransport().QueryInterface( + Ci.nsIWebTransport + ); + + await new Promise(resolve => { + let listener = new WebTransportListener().QueryInterface( + Ci.WebTransportSessionEventListener + ); + listener.ready = resolve; + + webTransport.asyncConnect( + NetUtil.newURI(`https://${host}/success`), + Services.scriptSecurityManager.getSystemPrincipal(), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + listener + ); + }); + + let stream = await streamCreatePromise(webTransport, false); + let outputStream = stream.outputStream; + + let data = "1234567890ABC"; + outputStream.write(data, data.length); + + // We need some time to send the packet out. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + + let stats = await sendStreamStatsPromise(stream); + Assert.equal(stats.bytesSent, data.length); + + webTransport.closeSession(0, ""); +}); + +add_task(async function test_wt_receive_stream_and_stats() { + let webTransport = NetUtil.newWebTransport().QueryInterface( + Ci.nsIWebTransport + ); + + let listener = new WebTransportListener().QueryInterface( + Ci.WebTransportSessionEventListener + ); + + let pReady = new Promise(resolve => { + listener.ready = resolve; + }); + let pStreamReady = new Promise(resolve => { + listener.streamAvailable = resolve; + }); + webTransport.asyncConnect( + NetUtil.newURI(`https://${host}/create_unidi_stream_and_hello`), + Services.scriptSecurityManager.getSystemPrincipal(), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + listener + ); + + await pReady; + let stream = await pStreamReady; + + let data = await new Promise(resolve => { + let handler = new inputStreamReader().QueryInterface( + Ci.nsIInputStreamCallback + ); + handler.finish = resolve; + let inputStream = stream.inputStream; + inputStream.asyncWait(handler, 0, 0, Services.tm.currentThread); + }); + + info("data: " + data); + Assert.equal(data, "qwerty"); + + let stats = await receiveStreamStatsPromise(stream); + Assert.equal(stats.bytesReceived, data.length); + + stream.sendStopSending(0); + + webTransport.closeSession(0, ""); +}); + +add_task(async function test_wt_outgoing_bidi_stream() { + let webTransport = NetUtil.newWebTransport().QueryInterface( + Ci.nsIWebTransport + ); + + await new Promise(resolve => { + let listener = new WebTransportListener().QueryInterface( + Ci.WebTransportSessionEventListener + ); + listener.ready = resolve; + + webTransport.asyncConnect( + NetUtil.newURI(`https://${host}/success`), + Services.scriptSecurityManager.getSystemPrincipal(), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + listener + ); + }); + + let stream = await streamCreatePromise(webTransport, true); + let outputStream = stream.outputStream; + + let data = "1234567"; + outputStream.write(data, data.length); + + let received = await new Promise(resolve => { + let handler = new inputStreamReader().QueryInterface( + Ci.nsIInputStreamCallback + ); + handler.finish = resolve; + let inputStream = stream.inputStream; + inputStream.asyncWait(handler, 0, 0, Services.tm.currentThread); + }); + + info("received: " + received); + Assert.equal(received, data); + + let stats = await sendStreamStatsPromise(stream); + Assert.equal(stats.bytesSent, data.length); + + stats = await receiveStreamStatsPromise(stream); + Assert.equal(stats.bytesReceived, data.length); + + webTransport.closeSession(0, ""); +}); + +add_task(async function test_wt_incoming_bidi_stream() { + let webTransport = NetUtil.newWebTransport().QueryInterface( + Ci.nsIWebTransport + ); + + let listener = new WebTransportListener().QueryInterface( + Ci.WebTransportSessionEventListener + ); + + let pReady = new Promise(resolve => { + listener.ready = resolve; + }); + let pStreamReady = new Promise(resolve => { + listener.streamAvailable = resolve; + }); + webTransport.asyncConnect( + NetUtil.newURI(`https://${host}/create_bidi_stream`), + Services.scriptSecurityManager.getSystemPrincipal(), + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + listener + ); + + await pReady; + let stream = await pStreamReady; + + let outputStream = stream.outputStream; + + let data = "12345678"; + outputStream.write(data, data.length); + + let received = await new Promise(resolve => { + let handler = new inputStreamReader().QueryInterface( + Ci.nsIInputStreamCallback + ); + handler.finish = resolve; + let inputStream = stream.inputStream; + inputStream.asyncWait(handler, 0, 0, Services.tm.currentThread); + }); + + info("received: " + received); + Assert.equal(received, data); + + let stats = await sendStreamStatsPromise(stream); + Assert.equal(stats.bytesSent, data.length); + + stats = await receiveStreamStatsPromise(stream); + Assert.equal(stats.bytesReceived, data.length); + + webTransport.closeSession(0, ""); +}); diff --git a/netwerk/test/unit/test_xmlhttprequest.js b/netwerk/test/unit/test_xmlhttprequest.js new file mode 100644 index 0000000000..6e478effc8 --- /dev/null +++ b/netwerk/test/unit/test_xmlhttprequest.js @@ -0,0 +1,55 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var testpath = "/simple"; +var httpbody = "<?xml version='1.0' ?><root>0123456789</root>"; + +function createXHR(async) { + var xhr = new XMLHttpRequest(); + xhr.open( + "GET", + "http://localhost:" + httpserver.identity.primaryPort + testpath, + async + ); + return xhr; +} + +function checkResults(xhr) { + if (xhr.readyState != 4) { + return false; + } + + Assert.equal(xhr.status, 200); + Assert.equal(xhr.responseText, httpbody); + + var root_node = xhr.responseXML.getElementsByTagName("root").item(0); + Assert.equal(root_node.firstChild.data, "0123456789"); + return true; +} + +function run_test() { + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + + // Test sync XHR sending + var sync = createXHR(false); + sync.send(null); + checkResults(sync); + + // Test async XHR sending + let async = createXHR(true); + async.addEventListener("readystatechange", function (event) { + if (checkResults(async)) { + httpserver.stop(do_test_finished); + } + }); + async.send(null); + do_test_pending(); +} + +function serverHandler(metadata, response) { + response.setHeader("Content-Type", "text/xml", false); + response.bodyOutputStream.write(httpbody, httpbody.length); +} diff --git a/netwerk/test/unit/trr_common.js b/netwerk/test/unit/trr_common.js new file mode 100644 index 0000000000..62cf4d094d --- /dev/null +++ b/netwerk/test/unit/trr_common.js @@ -0,0 +1,1233 @@ +/* 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"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_trr.js */ +/* import-globals-from head_http3.js */ + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const TRR_Domain = "foo.example.com"; + +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +const gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); + +async function SetParentalControlEnabled(aEnabled) { + let parentalControlsService = { + parentalControlsEnabled: aEnabled, + QueryInterface: ChromeUtils.generateQI(["nsIParentalControlsService"]), + }; + let cid = MockRegistrar.register( + "@mozilla.org/parental-controls-service;1", + parentalControlsService + ); + Services.dns.reloadParentalControlEnabled(); + MockRegistrar.unregister(cid); +} + +let runningOHTTPTests = false; +let h2Port; + +function setModeAndURIForODoH(mode, path) { + Services.prefs.setIntPref("network.trr.mode", mode); + if (path.substr(0, 4) == "doh?") { + path = path.replace("doh?", "odoh?"); + } + + Services.prefs.setCharPref("network.trr.odoh.target_path", `${path}`); +} + +function setModeAndURIForOHTTP(mode, path, domain) { + Services.prefs.setIntPref("network.trr.mode", mode); + if (domain) { + Services.prefs.setCharPref( + "network.trr.ohttp.uri", + `https://${domain}:${h2Port}/${path}` + ); + } else { + Services.prefs.setCharPref( + "network.trr.ohttp.uri", + `https://${TRR_Domain}:${h2Port}/${path}` + ); + } +} + +function setModeAndURI(mode, path, domain) { + if (runningOHTTPTests) { + setModeAndURIForOHTTP(mode, path, domain); + } else { + Services.prefs.setIntPref("network.trr.mode", mode); + if (domain) { + Services.prefs.setCharPref( + "network.trr.uri", + `https://${domain}:${h2Port}/${path}` + ); + } else { + Services.prefs.setCharPref( + "network.trr.uri", + `https://${TRR_Domain}:${h2Port}/${path}` + ); + } + } +} + +async function test_A_record() { + info("Verifying a basic A record"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); // TRR-first + await new TRRDNSListener("bar.example.com", "2.2.2.2"); + + info("Verifying a basic A record - without bootstrapping"); + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=3.3.3.3"); // TRR-only + + // Clear bootstrap address and add DoH endpoint hostname to local domains + Services.prefs.clearUserPref("network.trr.bootstrapAddr"); + Services.prefs.setCharPref("network.dns.localDomains", TRR_Domain); + + await new TRRDNSListener("bar.example.com", "3.3.3.3"); + + Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1"); + Services.prefs.clearUserPref("network.dns.localDomains"); + + info("Verify that the cached record is used when DoH endpoint is down"); + // Don't clear the cache. That is what we're checking. + setModeAndURI(3, "404"); + + await new TRRDNSListener("bar.example.com", "3.3.3.3"); + info("verify working credentials in DOH request"); + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=4.4.4.4&auth=true"); + Services.prefs.setCharPref("network.trr.credentials", "user:password"); + + await new TRRDNSListener("bar.example.com", "4.4.4.4"); + + info("Verify failing credentials in DOH request"); + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=4.4.4.4&auth=true"); + Services.prefs.setCharPref("network.trr.credentials", "evil:person"); + + let { inStatus } = await new TRRDNSListener( + "wrong.example.com", + undefined, + false + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + Services.prefs.clearUserPref("network.trr.credentials"); +} + +async function test_AAAA_records() { + info("Verifying AAAA record"); + + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=2020:2020::2020&delayIPv4=100"); + + await new TRRDNSListener("aaaa.example.com", "2020:2020::2020"); + + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=2020:2020::2020&delayIPv6=100"); + + await new TRRDNSListener("aaaa.example.com", "2020:2020::2020"); + + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=2020:2020::2020"); + + await new TRRDNSListener("aaaa.example.com", "2020:2020::2020"); +} + +async function test_RFC1918() { + info("Verifying that RFC1918 address from the server is rejected by default"); + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=192.168.0.1"); + + let { inStatus } = await new TRRDNSListener( + "rfc1918.example.com", + undefined, + false + ); + + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + setModeAndURI(3, "doh?responseIP=::ffff:192.168.0.1"); + ({ inStatus } = await new TRRDNSListener( + "rfc1918-ipv6.example.com", + undefined, + false + )); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + info("Verify RFC1918 address from the server is fine when told so"); + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=192.168.0.1"); + Services.prefs.setBoolPref("network.trr.allow-rfc1918", true); + await new TRRDNSListener("rfc1918.example.com", "192.168.0.1"); + setModeAndURI(3, "doh?responseIP=::ffff:192.168.0.1"); + + await new TRRDNSListener("rfc1918-ipv6.example.com", "::ffff:192.168.0.1"); + + Services.prefs.clearUserPref("network.trr.allow-rfc1918"); +} + +async function test_GET_ECS() { + info("Verifying resolution via GET with ECS disabled"); + Services.dns.clearCache(true); + // The template part should be discarded + setModeAndURI(3, "doh{?dns}"); + Services.prefs.setBoolPref("network.trr.useGET", true); + Services.prefs.setBoolPref("network.trr.disable-ECS", true); + + await new TRRDNSListener("ecs.example.com", "5.5.5.5"); + + info("Verifying resolution via GET with ECS enabled"); + Services.dns.clearCache(true); + setModeAndURI(3, "doh"); + Services.prefs.setBoolPref("network.trr.disable-ECS", false); + + await new TRRDNSListener("get.example.com", "5.5.5.5"); + + Services.prefs.clearUserPref("network.trr.useGET"); + Services.prefs.clearUserPref("network.trr.disable-ECS"); +} + +async function test_timeout_mode3() { + info("Verifying that a short timeout causes failure with a slow server"); + Services.dns.clearCache(true); + // First, mode 3. + setModeAndURI(3, "doh?noResponse=true"); + Services.prefs.setIntPref("network.trr.request_timeout_ms", 10); + Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10); + + let { inStatus } = await new TRRDNSListener( + "timeout.example.com", + undefined, + false + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + // Now for mode 2 + Services.dns.clearCache(true); + setModeAndURI(2, "doh?noResponse=true"); + + await new TRRDNSListener("timeout.example.com", "127.0.0.1"); // Should fallback + + Services.prefs.clearUserPref("network.trr.request_timeout_ms"); + Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms"); +} + +async function test_trr_retry() { + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", false); + + info("Test fallback to native"); + Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", false); + setModeAndURI(2, "doh?noResponse=true"); + Services.prefs.setIntPref("network.trr.request_timeout_ms", 10); + Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10); + + await new TRRDNSListener("timeout.example.com", { + expectedAnswer: "127.0.0.1", + }); + + Services.prefs.clearUserPref("network.trr.request_timeout_ms"); + Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms"); + + info("Test Retry Success"); + Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", true); + + let chan = makeChan( + `https://foo.example.com:${h2Port}/reset-doh-request-count`, + Ci.nsIRequest.TRR_DISABLED_MODE + ); + await new Promise(resolve => + chan.asyncOpen(new ChannelListener(resolve, null)) + ); + + setModeAndURI(2, "doh?responseIP=2.2.2.2&retryOnDecodeFailure=true"); + await new TRRDNSListener("retry_ok.example.com", "2.2.2.2"); + + info("Test Retry Failed"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true"); + await new TRRDNSListener("retry_ng.example.com", "127.0.0.1"); +} + +async function test_strict_native_fallback() { + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", true); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", true); + + info("First a timeout case"); + setModeAndURI(2, "doh?noResponse=true"); + Services.prefs.setIntPref("network.trr.request_timeout_ms", 10); + Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10); + Services.prefs.setIntPref( + "network.trr.strict_fallback_request_timeout_ms", + 10 + ); + + Services.prefs.setBoolPref( + "network.trr.strict_native_fallback_allow_timeouts", + false + ); + + let { inStatus } = await new TRRDNSListener( + "timeout.example.com", + undefined, + false + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + Services.dns.clearCache(true); + await new TRRDNSListener("timeout.example.com", undefined, false); + + Services.dns.clearCache(true); + Services.prefs.setBoolPref( + "network.trr.strict_native_fallback_allow_timeouts", + true + ); + await new TRRDNSListener("timeout.example.com", { + expectedAnswer: "127.0.0.1", + }); + + Services.prefs.setBoolPref( + "network.trr.strict_native_fallback_allow_timeouts", + false + ); + + info("Now a connection error"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + Services.prefs.clearUserPref("network.trr.request_timeout_ms"); + Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms"); + Services.prefs.clearUserPref( + "network.trr.strict_fallback_request_timeout_ms" + ); + ({ inStatus } = await new TRRDNSListener("closeme.com", undefined, false)); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + info("Now a decode error"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true"); + ({ inStatus } = await new TRRDNSListener( + "bar.example.com", + undefined, + false + )); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + if (!mozinfo.socketprocess_networking) { + // Confirmation state isn't passed cross-process. + info("Now with confirmation failed - should fallback"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true"); + Services.prefs.setCharPref("network.trr.confirmationNS", "example.com"); + await TestUtils.waitForCondition( + // 3 => CONFIRM_FAILED, 4 => CONFIRM_TRYING_FAILED + () => + Services.dns.currentTrrConfirmationState == 3 || + Services.dns.currentTrrConfirmationState == 4, + `Timed out waiting for confirmation failure. Currently ${Services.dns.currentTrrConfirmationState}`, + 1, + 5000 + ); + await new TRRDNSListener("bar.example.com", "127.0.0.1"); // Should fallback + } + + info("Now a successful case."); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + if (!mozinfo.socketprocess_networking) { + // Only need to reset confirmation state if we messed with it before. + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + await TestUtils.waitForCondition( + // 5 => CONFIRM_DISABLED + () => Services.dns.currentTrrConfirmationState == 5, + `Timed out waiting for confirmation disabled. Currently ${Services.dns.currentTrrConfirmationState}`, + 1, + 5000 + ); + } + await new TRRDNSListener("bar.example.com", "2.2.2.2"); + + info("Now without strict fallback mode, timeout case"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?noResponse=true"); + Services.prefs.setIntPref("network.trr.request_timeout_ms", 10); + Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10); + Services.prefs.setIntPref( + "network.trr.strict_fallback_request_timeout_ms", + 10 + ); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", false); + + await new TRRDNSListener("timeout.example.com", "127.0.0.1"); // Should fallback + + info("Now a connection error"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + Services.prefs.clearUserPref("network.trr.request_timeout_ms"); + Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms"); + Services.prefs.clearUserPref( + "network.trr.strict_fallback_request_timeout_ms" + ); + await new TRRDNSListener("closeme.com", "127.0.0.1"); // Should fallback + + info("Now a decode error"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true"); + await new TRRDNSListener("bar.example.com", "127.0.0.1"); // Should fallback + + Services.prefs.setBoolPref("network.trr.strict_native_fallback", false); + Services.prefs.clearUserPref("network.trr.request_timeout_ms"); + Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms"); + Services.prefs.clearUserPref( + "network.trr.strict_fallback_request_timeout_ms" + ); +} + +async function test_no_answers_fallback() { + info("Verfiying that we correctly fallback to Do53 when no answers from DoH"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=none"); // TRR-first + + await new TRRDNSListener("confirm.example.com", "127.0.0.1"); + + info("Now in strict mode - no fallback"); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", true); + Services.dns.clearCache(true); + await new TRRDNSListener("confirm.example.com", "127.0.0.1"); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", false); +} + +async function test_404_fallback() { + info("Verfiying that we correctly fallback to Do53 when DoH sends 404"); + Services.dns.clearCache(true); + setModeAndURI(2, "404"); // TRR-first + + await new TRRDNSListener("test404.example.com", "127.0.0.1"); + + info("Now in strict mode - no fallback"); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", true); + Services.dns.clearCache(true); + let { inStatus } = await new TRRDNSListener("test404.example.com", { + expectedSuccess: false, + }); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", false); +} + +async function test_mode_1_and_4() { + info("Verifying modes 1 and 4 are treated as TRR-off"); + for (let mode of [1, 4]) { + Services.dns.clearCache(true); + setModeAndURI(mode, "doh?responseIP=2.2.2.2"); + Assert.equal( + Services.dns.currentTrrMode, + 5, + "Effective TRR mode should be 5" + ); + } +} + +async function test_CNAME() { + info("Checking that we follow a CNAME correctly"); + Services.dns.clearCache(true); + // The dns-cname path alternates between sending us a CNAME pointing to + // another domain, and an A record. If we follow the cname correctly, doing + // a lookup with this path as the DoH URI should resolve to that A record. + setModeAndURI(3, "dns-cname"); + + await new TRRDNSListener("cname.example.com", "99.88.77.66"); + + info("Verifying that we bail out when we're thrown into a CNAME loop"); + Services.dns.clearCache(true); + // First mode 3. + setModeAndURI(3, "doh?responseIP=none&cnameloop=true"); + + let { inStatus } = await new TRRDNSListener( + "test18.example.com", + undefined, + false + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + // Now mode 2. + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=none&cnameloop=true"); + + await new TRRDNSListener("test20.example.com", "127.0.0.1"); // Should fallback + + info("Check that we correctly handle CNAME bundled with an A record"); + Services.dns.clearCache(true); + // "dns-cname-a" path causes server to send a CNAME as well as an A record + setModeAndURI(3, "dns-cname-a"); + + await new TRRDNSListener("cname-a.example.com", "9.8.7.6"); +} + +async function test_name_mismatch() { + info("Verify that records that don't match the requested name are rejected"); + Services.dns.clearCache(true); + // Setting hostname param tells server to always send record for bar.example.com + // regardless of what was requested. + setModeAndURI(3, "doh?hostname=mismatch.example.com"); + + let { inStatus } = await new TRRDNSListener( + "bar.example.com", + undefined, + false + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); +} + +async function test_mode_2() { + info("Checking that TRR result is used in mode 2"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=192.192.192.192"); + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + Services.prefs.setCharPref("network.trr.builtin-excluded-domains", ""); + + await new TRRDNSListener("bar.example.com", "192.192.192.192"); + + info("Now in strict mode"); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", true); + Services.dns.clearCache(true); + await new TRRDNSListener("bar.example.com", "192.192.192.192"); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", false); +} + +async function test_excluded_domains() { + info("Checking that Do53 is used for names in excluded-domains list"); + for (let strictMode of [true, false]) { + info("Strict mode: " + strictMode); + Services.prefs.setBoolPref( + "network.trr.strict_native_fallback", + strictMode + ); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=192.192.192.192"); + Services.prefs.setCharPref( + "network.trr.excluded-domains", + "bar.example.com" + ); + + await new TRRDNSListener("bar.example.com", "127.0.0.1"); // Do53 result + + Services.dns.clearCache(true); + Services.prefs.setCharPref("network.trr.excluded-domains", "example.com"); + + await new TRRDNSListener("bar.example.com", "127.0.0.1"); + + Services.dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.excluded-domains", + "foo.test.com, bar.example.com" + ); + await new TRRDNSListener("bar.example.com", "127.0.0.1"); + + Services.dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.excluded-domains", + "bar.example.com, foo.test.com" + ); + + await new TRRDNSListener("bar.example.com", "127.0.0.1"); + + Services.prefs.clearUserPref("network.trr.excluded-domains"); + } +} + +function topicObserved(topic) { + return new Promise(resolve => { + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == topic) { + Services.obs.removeObserver(observer, topic); + resolve(aData); + } + }, + }; + Services.obs.addObserver(observer, topic); + }); +} + +async function test_captiveportal_canonicalURL() { + info("Check that captivedetect.canonicalURL is resolved via native DNS"); + for (let strictMode of [true, false]) { + info("Strict mode: " + strictMode); + Services.prefs.setBoolPref( + "network.trr.strict_native_fallback", + strictMode + ); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + + const cpServer = new HttpServer(); + cpServer.registerPathHandler( + "/cp", + function handleRawData(request, response) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + response.bodyOutputStream.write("data", 4); + } + ); + cpServer.start(-1); + cpServer.identity.setPrimary( + "http", + "detectportal.firefox.com", + cpServer.identity.primaryPort + ); + let cpPromise = topicObserved("captive-portal-login"); + + Services.prefs.setCharPref( + "captivedetect.canonicalURL", + `http://detectportal.firefox.com:${cpServer.identity.primaryPort}/cp` + ); + Services.prefs.setBoolPref("network.captive-portal-service.testMode", true); + Services.prefs.setBoolPref("network.captive-portal-service.enabled", true); + + // The captive portal has to have used native DNS, otherwise creating + // a socket to a non-local IP would trigger a crash. + await cpPromise; + // Simply resolving the captive portal domain should still use TRR + await new TRRDNSListener("detectportal.firefox.com", "2.2.2.2"); + + Services.prefs.clearUserPref("network.captive-portal-service.enabled"); + Services.prefs.clearUserPref("network.captive-portal-service.testMode"); + Services.prefs.clearUserPref("captivedetect.canonicalURL"); + + await new Promise(resolve => cpServer.stop(resolve)); + } +} + +async function test_parentalcontrols() { + info("Check that DoH isn't used when parental controls are enabled"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + await SetParentalControlEnabled(true); + await new TRRDNSListener("www.example.com", "127.0.0.1"); + await SetParentalControlEnabled(false); + + info("Now in strict mode"); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", true); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + await SetParentalControlEnabled(true); + await new TRRDNSListener("www.example.com", "127.0.0.1"); + await SetParentalControlEnabled(false); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", false); +} + +async function test_builtin_excluded_domains() { + info("Verifying Do53 is used for domains in builtin-excluded-domians list"); + for (let strictMode of [true, false]) { + info("Strict mode: " + strictMode); + Services.prefs.setBoolPref( + "network.trr.strict_native_fallback", + strictMode + ); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2"); + + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "bar.example.com" + ); + await new TRRDNSListener("bar.example.com", "127.0.0.1"); + + Services.dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "example.com" + ); + await new TRRDNSListener("bar.example.com", "127.0.0.1"); + + Services.dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "foo.test.com, bar.example.com" + ); + await new TRRDNSListener("bar.example.com", "127.0.0.1"); + await new TRRDNSListener("foo.test.com", "127.0.0.1"); + } +} + +async function test_excluded_domains_mode3() { + info("Checking Do53 is used for names in excluded-domains list in mode 3"); + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=192.192.192.192"); + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + Services.prefs.setCharPref("network.trr.builtin-excluded-domains", ""); + + await new TRRDNSListener("excluded", "192.192.192.192", true); + + Services.dns.clearCache(true); + Services.prefs.setCharPref("network.trr.excluded-domains", "excluded"); + + await new TRRDNSListener("excluded", "127.0.0.1"); + + // Test .local + Services.dns.clearCache(true); + Services.prefs.setCharPref("network.trr.excluded-domains", "excluded,local"); + + await new TRRDNSListener("test.local", "127.0.0.1"); + + // Test .other + Services.dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.excluded-domains", + "excluded,local,other" + ); + + await new TRRDNSListener("domain.other", "127.0.0.1"); +} + +async function test25e() { + info("Check captivedetect.canonicalURL is resolved via native DNS in mode 3"); + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=192.192.192.192"); + + const cpServer = new HttpServer(); + cpServer.registerPathHandler( + "/cp", + function handleRawData(request, response) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + response.bodyOutputStream.write("data", 4); + } + ); + cpServer.start(-1); + cpServer.identity.setPrimary( + "http", + "detectportal.firefox.com", + cpServer.identity.primaryPort + ); + let cpPromise = topicObserved("captive-portal-login"); + + Services.prefs.setCharPref( + "captivedetect.canonicalURL", + `http://detectportal.firefox.com:${cpServer.identity.primaryPort}/cp` + ); + Services.prefs.setBoolPref("network.captive-portal-service.testMode", true); + Services.prefs.setBoolPref("network.captive-portal-service.enabled", true); + + // The captive portal has to have used native DNS, otherwise creating + // a socket to a non-local IP would trigger a crash. + await cpPromise; + // // Simply resolving the captive portal domain should still use TRR + await new TRRDNSListener("detectportal.firefox.com", "192.192.192.192"); + + Services.prefs.clearUserPref("network.captive-portal-service.enabled"); + Services.prefs.clearUserPref("network.captive-portal-service.testMode"); + Services.prefs.clearUserPref("captivedetect.canonicalURL"); + + await new Promise(resolve => cpServer.stop(resolve)); +} + +async function test_parentalcontrols_mode3() { + info("Check DoH isn't used when parental controls are enabled in mode 3"); + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=192.192.192.192"); + await SetParentalControlEnabled(true); + await new TRRDNSListener("www.example.com", "127.0.0.1"); + await SetParentalControlEnabled(false); +} + +async function test_builtin_excluded_domains_mode3() { + info("Check Do53 used for domains in builtin-excluded-domians list, mode 3"); + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=192.192.192.192"); + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "excluded" + ); + + await new TRRDNSListener("excluded", "127.0.0.1"); + + // Test .local + Services.dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "excluded,local" + ); + + await new TRRDNSListener("test.local", "127.0.0.1"); + + // Test .other + Services.dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "excluded,local,other" + ); + + await new TRRDNSListener("domain.other", "127.0.0.1"); +} + +async function count_cookies() { + info("Check that none of the requests have set any cookies."); + Assert.equal(Services.cookies.countCookiesFromHost("example.com"), 0); + Assert.equal(Services.cookies.countCookiesFromHost("foo.example.com."), 0); +} + +async function test_connection_closed() { + info("Check we handle it correctly when the connection is closed"); + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=2.2.2.2"); + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + // We don't need to wait for 30 seconds for the request to fail + Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 500); + // bootstrap + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1"); + + await new TRRDNSListener("bar.example.com", "2.2.2.2"); + + // makes the TRR connection shut down. + let { inStatus } = await new TRRDNSListener("closeme.com", undefined, false); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + await new TRRDNSListener("bar2.example.com", "2.2.2.2"); + + // No bootstrap this time + Services.prefs.clearUserPref("network.trr.bootstrapAddr"); + + Services.dns.clearCache(true); + Services.prefs.setCharPref("network.trr.excluded-domains", "excluded,local"); + Services.prefs.setCharPref("network.dns.localDomains", TRR_Domain); + + await new TRRDNSListener("bar.example.com", "2.2.2.2"); + + // makes the TRR connection shut down. + ({ inStatus } = await new TRRDNSListener("closeme.com", undefined, false)); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + await new TRRDNSListener("bar2.example.com", "2.2.2.2"); + + // No local domains either + Services.dns.clearCache(true); + Services.prefs.setCharPref("network.trr.excluded-domains", "excluded"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.trr.bootstrapAddr"); + + await new TRRDNSListener("bar.example.com", "2.2.2.2"); + + // makes the TRR connection shut down. + ({ inStatus } = await new TRRDNSListener("closeme.com", undefined, false)); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + await new TRRDNSListener("bar2.example.com", "2.2.2.2"); + + // Now make sure that even in mode 3 without a bootstrap address + // we are able to restart the TRR connection if it drops - the TRR service + // channel will use regular DNS to resolve the TRR address. + Services.dns.clearCache(true); + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + Services.prefs.setCharPref("network.trr.builtin-excluded-domains", ""); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.trr.bootstrapAddr"); + + await new TRRDNSListener("bar.example.com", "2.2.2.2"); + + // makes the TRR connection shut down. + ({ inStatus } = await new TRRDNSListener("closeme.com", undefined, false)); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + Services.dns.clearCache(true); + await new TRRDNSListener("bar2.example.com", "2.2.2.2"); + + // This test exists to document what happens when we're in TRR only mode + // and we don't set a bootstrap address. We use DNS to resolve the + // initial URI, but if the connection fails, we don't fallback to DNS + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=9.9.9.9"); + Services.prefs.setCharPref("network.dns.localDomains", "closeme.com"); + Services.prefs.clearUserPref("network.trr.bootstrapAddr"); + + await new TRRDNSListener("bar.example.com", "9.9.9.9"); + + // makes the TRR connection shut down. Should fallback to DNS + await new TRRDNSListener("closeme.com", "127.0.0.1"); + // TRR should be back up again + await new TRRDNSListener("bar2.example.com", "9.9.9.9"); +} + +async function test_fetch_time() { + info("Verifying timing"); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2&delayIPv4=20"); + + await new TRRDNSListener("bar_time.example.com", "2.2.2.2", true, 20); + + // gets an error from DoH. It will fall back to regular DNS. The TRR timing should be 0. + Services.dns.clearCache(true); + setModeAndURI(2, "404&delayIPv4=20"); + + await new TRRDNSListener("bar_time1.example.com", "127.0.0.1", true, 0); + + // check an excluded domain. It should fall back to regular DNS. The TRR timing should be 0. + Services.prefs.setCharPref( + "network.trr.excluded-domains", + "bar_time2.example.com" + ); + for (let strictMode of [true, false]) { + info("Strict mode: " + strictMode); + Services.prefs.setBoolPref( + "network.trr.strict_native_fallback", + strictMode + ); + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=2.2.2.2&delayIPv4=20"); + await new TRRDNSListener("bar_time2.example.com", "127.0.0.1", true, 0); + } + + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + + // verify RFC1918 address from the server is rejected and the TRR timing will be not set because the response will be from the native resolver. + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=192.168.0.1&delayIPv4=20"); + await new TRRDNSListener("rfc1918_time.example.com", "127.0.0.1", true, 0); +} + +async function test_fqdn() { + info("Test that we handle FQDN encoding and decoding properly"); + Services.dns.clearCache(true); + setModeAndURI(3, "doh?responseIP=9.8.7.6"); + + await new TRRDNSListener("fqdn.example.org.", "9.8.7.6"); + + // GET + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.useGET", true); + await new TRRDNSListener("fqdn_get.example.org.", "9.8.7.6"); + + Services.prefs.clearUserPref("network.trr.useGET"); +} + +async function test_ipv6_trr_fallback() { + info("Testing fallback with ipv6"); + Services.dns.clearCache(true); + + setModeAndURI(2, "doh?responseIP=4.4.4.4"); + const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride + ); + gOverride.addIPOverride("ipv6.host.com", "1:1::2"); + + // Should not fallback to Do53 because A request for ipv6.host.com returns + // 4.4.4.4 + let { inStatus } = await new TRRDNSListener("ipv6.host.com", { + flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV4, + expectedSuccess: false, + }); + equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); + + // This time both requests fail, so we do fall back + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=none"); + await new TRRDNSListener("ipv6.host.com", "1:1::2"); + + info("In strict mode, the lookup should fail when both reqs fail."); + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", true); + setModeAndURI(2, "doh?responseIP=none"); + await new TRRDNSListener("ipv6.host.com", "1:1::2"); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", false); + + override.clearOverrides(); +} + +async function test_ipv4_trr_fallback() { + info("Testing fallback with ipv4"); + Services.dns.clearCache(true); + + setModeAndURI(2, "doh?responseIP=1:2::3"); + const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride + ); + gOverride.addIPOverride("ipv4.host.com", "3.4.5.6"); + + // Should not fallback to Do53 because A request for ipv4.host.com returns + // 1:2::3 + let { inStatus } = await new TRRDNSListener("ipv4.host.com", { + flags: Ci.nsIDNSService.RESOLVE_DISABLE_IPV6, + expectedSuccess: false, + }); + equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); + + // This time both requests fail, so we do fall back + Services.dns.clearCache(true); + setModeAndURI(2, "doh?responseIP=none"); + await new TRRDNSListener("ipv4.host.com", "3.4.5.6"); + + // No fallback with strict mode. + Services.dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", true); + setModeAndURI(2, "doh?responseIP=none"); + await new TRRDNSListener("ipv4.host.com", "3.4.5.6"); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", false); + + override.clearOverrides(); +} + +async function test_no_retry_without_doh() { + info("Bug 1648147 - if the TRR returns 0.0.0.0 we should not retry with DNS"); + Services.prefs.setBoolPref("network.trr.fallback-on-zero-response", false); + + async function test(url, ip) { + setModeAndURI(2, `doh?responseIP=${ip}`); + + // Requests to 0.0.0.0 are usually directed to localhost, so let's use a port + // we know isn't being used - 666 (Doom) + let chan = makeChan(url, Ci.nsIRequest.TRR_DEFAULT_MODE); + let statusCounter = { + statusCount: {}, + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIProgressEventSink", + ]), + getInterface(iid) { + return this.QueryInterface(iid); + }, + onProgress(request, progress, progressMax) {}, + onStatus(request, status, statusArg) { + this.statusCount[status] = 1 + (this.statusCount[status] || 0); + }, + }; + chan.notificationCallbacks = statusCounter; + await new Promise(resolve => + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)) + ); + equal( + statusCounter.statusCount[0x4b000b], + 1, + "Expecting only one instance of NS_NET_STATUS_RESOLVED_HOST" + ); + equal( + statusCounter.statusCount[0x4b0007], + 1, + "Expecting only one instance of NS_NET_STATUS_CONNECTING_TO" + ); + } + + for (let strictMode of [true, false]) { + info("Strict mode: " + strictMode); + Services.prefs.setBoolPref( + "network.trr.strict_native_fallback", + strictMode + ); + await test(`http://unknown.ipv4.stuff:666/path`, "0.0.0.0"); + await test(`http://unknown.ipv6.stuff:666/path`, "::"); + } +} + +async function test_connection_reuse_and_cycling() { + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.request_timeout_ms", 500); + Services.prefs.setIntPref( + "network.trr.strict_fallback_request_timeout_ms", + 500 + ); + Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 500); + + setModeAndURI(2, `doh?responseIP=9.8.7.6`); + Services.prefs.setBoolPref("network.trr.strict_native_fallback", true); + Services.prefs.setCharPref("network.trr.confirmationNS", "example.com"); + await TestUtils.waitForCondition( + // 2 => CONFIRM_OK + () => Services.dns.currentTrrConfirmationState == 2, + `Timed out waiting for confirmation success. Currently ${Services.dns.currentTrrConfirmationState}`, + 1, + 5000 + ); + + // Setting conncycle=true in the URI. Server will start logging reqs. + // We will do a specific sequence of lookups, then fetch the log from + // the server and check that it matches what we'd expect. + setModeAndURI(2, `doh?responseIP=9.8.7.6&conncycle=true`); + await TestUtils.waitForCondition( + // 2 => CONFIRM_OK + () => Services.dns.currentTrrConfirmationState == 2, + `Timed out waiting for confirmation success. Currently ${Services.dns.currentTrrConfirmationState}`, + 1, + 5000 + ); + // Confirmation upon uri-change will have created one req. + + // Two reqs for each bar1 and bar2 - A + AAAA. + await new TRRDNSListener("bar1.example.org.", "9.8.7.6"); + await new TRRDNSListener("bar2.example.org.", "9.8.7.6"); + // Total so far: (1) + 2 + 2 = 5 + + // Two reqs that fail, one Confirmation req, two retried reqs that succeed. + await new TRRDNSListener("newconn.example.org.", "9.8.7.6"); + await TestUtils.waitForCondition( + // 2 => CONFIRM_OK + () => Services.dns.currentTrrConfirmationState == 2, + `Timed out waiting for confirmation success. Currently ${Services.dns.currentTrrConfirmationState}`, + 1, + 5000 + ); + // Total so far: (5) + 2 + 1 + 2 = 10 + + // Two reqs for each bar3 and bar4 . + await new TRRDNSListener("bar3.example.org.", "9.8.7.6"); + await new TRRDNSListener("bar4.example.org.", "9.8.7.6"); + // Total so far: (10) + 2 + 2 = 14. + + // Two reqs that fail, one Confirmation req, two retried reqs that succeed. + await new TRRDNSListener("newconn2.example.org.", "9.8.7.6"); + await TestUtils.waitForCondition( + // 2 => CONFIRM_OK + () => Services.dns.currentTrrConfirmationState == 2, + `Timed out waiting for confirmation success. Currently ${Services.dns.currentTrrConfirmationState}`, + 1, + 5000 + ); + // Total so far: (14) + 2 + 1 + 2 = 19 + + // Two reqs for each bar5 and bar6 . + await new TRRDNSListener("bar5.example.org.", "9.8.7.6"); + await new TRRDNSListener("bar6.example.org.", "9.8.7.6"); + // Total so far: (19) + 2 + 2 = 23 + + let chan = makeChan( + `https://foo.example.com:${h2Port}/get-doh-req-port-log`, + Ci.nsIRequest.TRR_DISABLED_MODE + ); + let dohReqPortLog = await new Promise(resolve => + chan.asyncOpen( + new ChannelListener((stuff, buffer) => { + resolve(JSON.parse(buffer)); + }) + ) + ); + + // Since the actual ports seen will vary at runtime, we use placeholders + // instead in our expected output definition. For example, if two entries + // both have "port1", it means they both should have the same port in the + // server's log. + // For reqs that fail and trigger a Confirmation + retry, the retried reqs + // might not re-use the new connection created for Confirmation due to a + // race, so we have an extra alternate expected port for them. This lets + // us test that they use *a* new port even if it's not *the* new port. + // Subsequent lookups are not affected, they will use the same conn as + // the Confirmation req. + let expectedLogTemplate = [ + ["example.com", "port1"], + ["bar1.example.org", "port1"], + ["bar1.example.org", "port1"], + ["bar2.example.org", "port1"], + ["bar2.example.org", "port1"], + ["newconn.example.org", "port1"], + ["newconn.example.org", "port1"], + ["example.com", "port2"], + ["newconn.example.org", "port2"], + ["newconn.example.org", "port2"], + ["bar3.example.org", "port2"], + ["bar3.example.org", "port2"], + ["bar4.example.org", "port2"], + ["bar4.example.org", "port2"], + ["newconn2.example.org", "port2"], + ["newconn2.example.org", "port2"], + ["example.com", "port3"], + ["newconn2.example.org", "port3"], + ["newconn2.example.org", "port3"], + ["bar5.example.org", "port3"], + ["bar5.example.org", "port3"], + ["bar6.example.org", "port3"], + ["bar6.example.org", "port3"], + ]; + + if (expectedLogTemplate.length != dohReqPortLog.length) { + // This shouldn't happen, and if it does, we'll fail the assertion + // below. But first dump the whole server-side log to help with + // debugging should we see a failure. Most likely cause would be + // that another consumer of TRR happened to make a request while + // the test was running and polluted the log. + info(dohReqPortLog); + } + + equal( + expectedLogTemplate.length, + dohReqPortLog.length, + "Correct number of req log entries" + ); + + let seenPorts = new Set(); + // This is essentially a symbol table - as we iterate through the log + // we will assign the actual seen port numbers to the placeholders. + let seenPortsByExpectedPort = new Map(); + + for (let i = 0; i < expectedLogTemplate.length; i++) { + let expectedName = expectedLogTemplate[i][0]; + let expectedPort = expectedLogTemplate[i][1]; + let seenName = dohReqPortLog[i][0]; + let seenPort = dohReqPortLog[i][1]; + info(`Checking log entry. Name: ${seenName}, Port: ${seenPort}`); + equal(expectedName, seenName, "Name matches for entry " + i); + if (!seenPortsByExpectedPort.has(expectedPort)) { + ok(!seenPorts.has(seenPort), "Port should not have been previously used"); + seenPorts.add(seenPort); + seenPortsByExpectedPort.set(expectedPort, seenPort); + } else { + equal( + seenPort, + seenPortsByExpectedPort.get(expectedPort), + "Connection was reused as expected" + ); + } + } +} diff --git a/netwerk/test/unit/xpcshell.ini b/netwerk/test/unit/xpcshell.ini new file mode 100644 index 0000000000..456e8ddfc5 --- /dev/null +++ b/netwerk/test/unit/xpcshell.ini @@ -0,0 +1,779 @@ + +[DEFAULT] +head = head_channels.js head_cache.js head_cache2.js head_cookies.js head_trr.js head_http3.js head_servers.js head_telemetry.js head_websocket.js head_webtransport.js +support-files = + http2-ca.pem + proxy-ca.pem + client-cert.p12 + data/cookies_v10.sqlite + data/image.png + data/system_root.lnk + data/test_psl.txt + data/test_readline1.txt + data/test_readline2.txt + data/test_readline3.txt + data/test_readline4.txt + data/test_readline5.txt + data/test_readline6.txt + data/test_readline7.txt + data/test_readline8.txt + data/signed_win.exe + socks_client_subprocess.js + test_link.desktop + test_link.url + test_link.lnk + ../../dns/effective_tld_names.dat + test_alt-data_cross_process.js + trr_common.js + test_http3_prio_helpers.js + http2_test_common.js + +# dom.serviceWorkers.enabled is currently set to false in StaticPrefList.yaml +# and enabled individually by app prefs, so for the xpcshell tests that involve +# interception, we need to explicitly enable the pref. +# Consider enabling it in StaticPrefList.yaml +# https://bugzilla.mozilla.org/show_bug.cgi?id=1816325 +# Several tests rely on redirecting to data: URIs, which was allowed for a long +# time but now forbidden. So we enable it just for these tests. +prefs = + dom.serviceWorkers.enabled=true + network.allow_redirect_to_data=true + +[test_trr_nat64.js] +run-sequentially = node server exceptions dont replay well +[test_nsIBufferedOutputStream_writeFrom_block.js] +[test_cache2-00-service-get.js] +[test_cache2-01-basic.js] +[test_cache2-01a-basic-readonly.js] +[test_cache2-01b-basic-datasize.js] +[test_cache2-01c-basic-hasmeta-only.js] +[test_cache2-01d-basic-not-wanted.js] +[test_cache2-01e-basic-bypass-if-busy.js] +[test_cache2-01f-basic-openTruncate.js] +[test_cache2-02-open-non-existing.js] +[test_cache2-02b-open-non-existing-and-doom.js] +[test_cache2-03-oncacheentryavail-throws.js] +[test_cache2-04-oncacheentryavail-throws2x.js] +[test_cache2-05-visit.js] +[test_cache2-06-pb-mode.js] +[test_cache2-07-visit-memory.js] +[test_cache2-07a-open-memory.js] +[test_cache2-08-evict-disk-by-memory-storage.js] +[test_cache2-09-evict-disk-by-uri.js] +[test_cache2-10-evict-direct.js] +[test_cache2-10b-evict-direct-immediate.js] +[test_cache2-11-evict-memory.js] +[test_cache2-12-evict-disk.js] +[test_cache2-13-evict-non-existing.js] +[test_cache2-14-concurent-readers.js] +[test_cache2-14b-concurent-readers-complete.js] +[test_cache2-15-conditional-304.js] +[test_cache2-16-conditional-200.js] +[test_cache2-17-evict-all.js] +[test_cache2-18-not-valid.js] +[test_cache2-19-range-206.js] +[test_cache2-20-range-200.js] +[test_cache2-21-anon-storage.js] +[test_cache2-22-anon-visit.js] +[test_cache2-23-read-over-chunk.js] +[test_cache2-24-exists.js] +[test_cache2-25-chunk-memory-limit.js] +[test_cache2-26-no-outputstream-open.js] +[test_cache2-27-force-valid-for.js] +[test_cache2-28-last-access-attrs.js] +# This test will be fixed in bug 1067931 +skip-if = true +[test_cache2-28a-OPEN_SECRETLY.js] +# This test will be fixed in bug 1067931 +skip-if = true +[test_cache2-29a-concurrent_read_resumable_entry_size_zero.js] +[test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js] +[test_cache2-29c-concurrent_read_half-interrupted.js] +[test_cache2-29d-concurrent_read_half-corrupted-206.js] +[test_cache2-29e-concurrent_read_half-non-206-response.js] +[test_cache2-30a-entry-pinning.js] +[test_cache2-30b-pinning-storage-clear.js] +[test_cache2-30c-pinning-deferred-doom.js] +[test_cache2-30d-pinning-WasEvicted-API.js] +[test_cache2-31-visit-all.js] +[test_cache2-32-clear-origin.js] +[test_partial_response_entry_size_smart_shrink.js] +[test_304_responses.js] +[test_421.js] +[test_307_redirect.js] +[test_NetUtil.js] +[test_URIs.js] +# Intermittent time-outs on Android, bug 1285020 +requesttimeoutfactor = 2 +[test_URIs2.js] +# Intermittent time-outs on Android, bug 1285020 +requesttimeoutfactor = 2 +[test_aboutblank.js] +[test_auth_jar.js] +[test_auth_proxy.js] +[test_authentication.js] +[test_ntlm_authentication.js] +[test_auth_multiple.js] +[test_authpromptwrapper.js] +[test_auth_dialog_permission.js] +[test_backgroundfilesaver.js] +[test_bug203271.js] +[test_bug248970_cache.js] +[test_bug248970_cookie.js] +[test_bug261425.js] +[test_bug263127.js] +[test_bug282432.js] +[test_bug321706.js] +[test_bug331825.js] +[test_bug336501.js] +[test_bug337744.js] +[test_bug368702.js] +[test_bug369787.js] +[test_bug371473.js] +[test_bug376844.js] +[test_bug376865.js] +[test_bug379034.js] +[test_bug380994.js] +[test_bug388281.js] +[test_bug396389.js] +[test_bug401564.js] +[test_bug411952.js] +[test_bug412945.js] +[test_bug414122.js] +[test_bug427957.js] +[test_bug429347.js] +[test_bug455311.js] +[test_bug468426.js] +[test_bug468594.js] +[test_bug470716.js] +[test_bug477578.js] +[test_bug479413.js] +[test_bug479485.js] +[test_bug482601.js] +[test_bug482934.js] +[test_bug490095.js] +[test_bug504014.js] +[test_bug510359.js] +[test_bug526789.js] +[test_bug528292.js] +[test_bug536324_64bit_content_length.js] +[test_bug540566.js] +[test_bug553970.js] +[test_bug561042.js] +[test_bug561276.js] +[test_bug580508.js] +[test_bug586908.js] +[test_bug596443.js] +[test_bug618835.js] +[test_bug633743.js] +[test_bug650522.js] +[test_bug650995.js] +[test_bug652761.js] +[test_bug654926.js] +[test_bug654926_doom_and_read.js] +[test_bug654926_test_seek.js] +[test_bug659569.js] +[test_bug660066.js] +[test_bug667087.js] +[test_bug667818.js] +[test_bug667907.js] +[test_bug669001.js] +[test_bug770243.js] +[test_bug894586.js] +# Allocating 4GB might actually succeed on 64 bit machines +skip-if = bits != 32 +[test_bug935499.js] +[test_bug1064258.js] +[test_bug1177909.js] +[test_bug1218029.js] +[test_udpsocket.js] +[test_udpsocket_offline.js] +[test_doomentry.js] +[test_dooh.js] +head = head_channels.js head_cache.js head_cookies.js head_trr.js head_http3.js trr_common.js +run-sequentially = node server exceptions dont replay well +skip-if = true # Disabled in 115esr, bug 1868042 +[test_cacheflags.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[test_cache_jar.js] +[test_cache-entry-id.js] +[test_channel_close.js] +skip-if = os == "win" && socketprocess_networking && !debug +[test_channel_long_domain.js] +[test_compareURIs.js] +[test_compressappend.js] +[test_content_encoding_gzip.js] +[test_content_sniffer.js] +[test_cookie_header.js] +[test_cookiejars.js] +[test_cookiejars_safebrowsing.js] +[test_cookies_async_failure.js] +skip-if = + os == 'linux' && bits == 64 && !debug #Bug 1553353 +[test_cookies_privatebrowsing.js] +[test_cookies_profile_close.js] +skip-if = os == "android" # Bug 1700483 +[test_cookies_read.js] +[test_cookies_sync_failure.js] +[test_cookies_thirdparty.js] +skip-if = appname == 'thunderbird' +reason = Thunderbird runs with fission enabled. This test requires fission.autostart=false. Bug 1749403. +[test_cookies_thirdparty_session.js] +skip-if = appname == 'thunderbird' +reason = Thunderbird runs with fission enabled. This test requires fission.autostart=false. Bug 1749403. +[test_cookies_upgrade_10.js] +[test_dns_cancel.js] +skip-if = verify +[test_data_protocol.js] +[test_dns_service.js] +[test_dns_offline.js] +[test_dns_onion.js] +[test_dns_originAttributes.js] +[test_dns_localredirect.js] +[test_dns_proxy_bypass.js] +[test_dns_disabled.js] +[test_domain_eviction.js] +[test_duplicate_headers.js] +[test_chunked_responses.js] +prefs = + security.allow_eval_with_system_principal=true +[test_content_length_underrun.js] +[test_event_sink.js] +[test_eviction.js] +[test_extract_charset_from_content_type.js] +[test_file_protocol.js] +[test_gio_protocol.js] +skip-if = (toolkit != 'gtk') +[test_filestreams.js] +[test_freshconnection.js] +[test_gre_resources.js] +[test_gzipped_206.js] +[test_head.js] +[test_header_Accept-Language.js] +[test_header_Accept-Language_case.js] +[test_headers.js] +[test_hostnameIsLocalIPAddress.js] +[test_hostnameIsSharedIPAddress.js] +[test_http_headers.js] +[test_httpauth.js] +[test_httpcancel.js] +[test_httpResponseTimeout.js] +skip-if = (os == "win" && socketprocess_networking) +[test_httpsuspend.js] +[test_idnservice.js] +[test_idn_blacklist.js] +[test_idn_urls.js] +[test_idna2008.js] +[test_idn_spoof.js] +[test_immutable.js] +run-sequentially = node server exceptions dont replay well +[test_localhost_offline.js] +[test_localstreams.js] +[test_large_port.js] +[test_mismatch_last-modified.js] +[test_MIME_params.js] +[test_mozTXTToHTMLConv.js] +[test_multipart_byteranges.js] +[test_multipart_streamconv.js] +[test_multipart_streamconv_missing_lead_boundary.js] +[test_multipart_streamconv_missing_boundary_lead_dashes.js] +[test_multipart_streamconv-byte-by-byte.js] +[test_nestedabout_serialize.js] +[test_net_addr.js] +# Bug 732363: test fails on windows for unknown reasons. +skip-if = os == "win" +[test_nojsredir.js] +[test_offline_status.js] +[test_origin.js] +[test_anonymous-coalescing.js] +[test_original_sent_received_head.js] +[test_parse_content_type.js] +[test_permmgr.js] +[test_plaintext_sniff.js] +skip-if = true # Causes sporatic oranges +[test_post.js] +[test_private_necko_channel.js] +[test_private_cookie_changed.js] +[test_progress.js] +[test_protocolproxyservice.js] +skip-if = + apple_silicon # bug 1707738 + (tsan && socketprocess_networking) # Bug 1808235 +[test_protocolproxyservice-async-filters.js] +[test_proxy-failover_canceled.js] +[test_proxy-failover_passing.js] +[test_proxy-replace_canceled.js] +[test_proxy-replace_passing.js] +[test_psl.js] +[test_range_requests.js] +[test_readline.js] +[test_redirect_veto.js] +[test_redirect-caching_canceled.js] +[test_redirect-caching_failure.js] +[test_redirect-caching_passing.js] +[test_redirect_canceled.js] +[test_redirect_failure.js] +[test_redirect_from_script.js] +[test_redirect_from_script_after-open_passing.js] +[test_redirect_passing.js] +[test_redirect_loop.js] +[test_redirect_baduri.js] +[test_redirect_different-protocol.js] +[test_redirect_protocol_telemetry.js] +[test_reentrancy.js] +[test_reopen.js] +[test_resumable_channel.js] +[test_resumable_truncate.js] +[test_safeoutputstream.js] +[test_schema_2_migration.js] +[test_schema_3_migration.js] +[test_schema_10_migration.js] +[test_simple.js] +[test_sockettransportsvc_available.js] +[test_socks.js] +skip-if = + (os == 'mac' && debug) #Bug 1140656 + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049 +# Bug 675039: test fails consistently on Android +fail-if = os == "android" +# http2 unit tests require us to have node available to run the spdy and http2 server +[test_http2.js] +run-sequentially = node server exceptions dont replay well +head = head_channels.js head_cache.js head_cookies.js head_trr.js head_http3.js http2_test_common.js +[test_altsvc.js] +run-sequentially = node server exceptions dont replay well +[test_speculative_connect.js] +[test_standardurl.js] +[test_standardurl_default_port.js] +[test_standardurl_port.js] +[test_streamcopier.js] +[test_traceable_channel.js] +[test_unescapestring.js] +[test_xmlhttprequest.js] +[test_XHR_redirects.js] +[test_bug826063.js] +[test_bug812167.js] +[test_tldservice_nextsubdomain.js] +[test_about_protocol.js] +[test_bug856978.js] +[test_unix_domain.js] +[test_addr_in_use_error.js] +[test_about_networking.js] +[test_ping_aboutnetworking.js] +skip-if = (verify && (os == 'mac')) +[test_referrer.js] +[test_referrer_cross_origin.js] +[test_referrer_policy.js] +[test_predictor.js] +[test_signature_extraction.js] +skip-if = os != "win" +[test_trr_ttl.js] +[test_synthesized_response.js] +[test_udp_multicast.js] +skip-if = + win10_2004 # Bug 1742311 +[test_redirect_history.js] +[test_reply_without_content_type.js] +[test_websocket_offline.js] +[test_be_conservative.js] +firefox-appdir = browser +[test_ech_grease.js] +firefox-appdir = browser +skip-if = + (tsan && socketprocess_networking) # Bug 1808236 +[test_be_conservative_error_handling.js] +firefox-appdir = browser +[test_tls_server.js] +firefox-appdir = browser +[test_tls_server_multiple_clients.js] +[test_1073747.js] +[test_safeoutputstream_append.js] +[test_suspend_channel_before_connect.js] +[test_suspend_channel_on_examine.js] +[test_suspend_channel_on_modified.js] +[test_inhibit_caching.js] +[test_dns_disable_ipv4.js] +[test_dns_disable_ipv6.js] +[test_bug1195415.js] +[test_cookie_blacklist.js] +[test_getHost.js] +[test_bug412457.js] +skip-if = appname == "thunderbird" +[test_bug464591.js] +skip-if = appname == "thunderbird" +[test_alt-data_simple.js] +skip-if = + win10_2004 && bits == 64 # Bug 1718292 + win11_2009 && bits == 64 && !debug # Bug 1797751 +run-sequentially = very high failure rate in parallel +[test_alt-data_stream.js] +[test_alt-data_too_big.js] +[test_alt-data_overwrite.js] +[test_alt-data_closeWithStatus.js] +[test_cache-control_request.js] +[test_bug1279246.js] +[test_throttlequeue.js] +[test_throttlechannel.js] +[test_throttling.js] +[test_separate_connections.js] +[test_trackingProtection_annotateChannels.js] +[test_ntlm_web_auth.js] +skip-if = os == "win" && os_version == "6.1" && bits == 32 # fails on Win7 +[test_ntlm_proxy_auth.js] +[test_ntlm_proxy_and_web_auth.js] +skip-if = os == "win" && os_version == "6.1" && bits == 32 # fails on Win7 +[test_race_cache_with_network.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[test_rcwn_always_cache_new_content.js] +[test_rcwn_interrupted.js] +[test_channel_priority.js] +[test_bug1312774_http1.js] +[test_bug1312782_http1.js] +skip-if = os == "android" # Bug 1700483 +[test_bug1355539_http1.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[test_bug1378385_http1.js] +[test_tls_flags_separate_connections.js] +[test_tls_flags.js] +skip-if = (os == "android" && processor == "x86_64") +[test_uri_mutator.js] +[test_bug1411316_http1.js] +[test_header_Server_Timing.js] +run-sequentially = node server exceptions dont replay well +[test_trr.js] +head = head_channels.js head_cache.js head_cookies.js head_trr.js head_http3.js trr_common.js +run-sequentially = very high failure rate in parallel +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[test_ioservice.js] +[test_substituting_protocol_handler.js] +[test_proxyconnect.js] +skip-if = + tsan + socketprocess_networking # Bug 1614708 +[test_captive_portal_service.js] +run-sequentially = node server exceptions dont replay well +[test_early_hint_listener_http2.js] +run-sequentially = node server exceptions dont replay well +[test_dns_by_type_resolve.js] +[test_network_connectivity_service.js] +[test_suspend_channel_on_authRetry.js] +[test_suspend_channel_on_examine_merged_response.js] +[test_bug1527293.js] +[test_stale-while-revalidate_negative.js] +[test_stale-while-revalidate_positive.js] +[test_stale-while-revalidate_loop.js] +[test_stale-while-revalidate_max-age-0.js] +[test_http1-proxy.js] +[test_http2-proxy.js] +run-sequentially = one http2 node proxy is used for all tests, this test is using global session counter +skip-if = os == "android" +[test_head_request_no_response_body.js] +[test_cache_204_response.js] +[test_http3.js] +skip-if = + os == 'android' # bug 1622901 + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931 +run-sequentially = http3server +[test_http3_421.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931 +run-sequentially = http3server +[test_http3_perf.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931 +run-sequentially = http3server +[test_http3_prio_disabled.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931 +run-sequentially = http3server +[test_http3_prio_enabled.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931 +run-sequentially = http3server +[test_http3_early_hint_listener.js] +skip-if = + os == 'android' + os == 'linux' # Bug 1773916 + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049 +run-sequentially = http3server +[test_node_execute.js] +[test_loadgroup_cancel.js] +[test_obs-fold.js] +[test_defaultURI.js] +[test_port_remapping.js] +skip-if = (os == "win" && socketprocess_networking) +[test_dns_override.js] +[test_dns_override_for_localhost.js] +[test_no_cookies_after_last_pb_exit.js] +[test_trr_httpssvc.js] +run-sequentially = node server exceptions dont replay well +[test_trr_case_sensitivity.js] +run-sequentially = node server exceptions dont replay well +[test_trr_proxy.js] +[test_trr_decoding.js] +[test_trr_cname_chain.js] +run-sequentially = node server exceptions dont replay well +[test_http_sfv.js] +[test_blob_channelname.js] +[test_altsvc_pref.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931 +[test_http3_alt_svc.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931 +run-sequentially = http3server +[test_use_httpssvc.js] +run-sequentially = node server exceptions dont replay well +[test_trr_additional_section.js] +run-sequentially = node server exceptions dont replay well +[test_trr_extended_error.js] +run-sequentially = node server exceptions dont replay well +[test_httpssvc_iphint.js] +run-sequentially = node server exceptions dont replay well +[test_multipart_streamconv_empty.js] +[test_httpssvc_priority.js] +run-sequentially = node server exceptions dont replay well +[test_trr_https_fallback.js] +skip-if = + asan + tsan + os == 'win' + os == 'android' +run-sequentially = node server exceptions dont replay well +[test_http3_trans_close.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931 +run-sequentially = http3server +[test_brotli_http.js] +[test_altsvc_http3.js] +skip-if = + true # Bug 1675008 + asan + tsan + os == 'android' +run-sequentially = http3server +[test_http3_fatal_stream_error.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931 +run-sequentially = node server exceptions dont replay well +[test_http3_large_post.js] +skip-if = + os == 'win' + os == 'android' +[test_http3_error_before_connect.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931 +run-sequentially = node server exceptions dont replay well +[test_http3_server_not_existing.js] +skip-if = os == 'android' +run-sequentially = node server exceptions dont replay well +[test_http3_fast_fallback.js] +skip-if = + os == 'win' + os == 'android' +run-sequentially = node server exceptions dont replay well +[test_cookie_ipv6.js] +[test_httpssvc_retry_with_ech.js] +skip-if = + os == 'android' # bug 1622901 + os == 'mac' && !debug + asan + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1808048 +run-sequentially = node server exceptions dont replay well +[test_httpssvc_ech_with_alpn.js] +skip-if = + os == 'android' # bug 1622901 + os == 'mac' && !debug + asan + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1808048 +run-sequentially = node server exceptions dont replay well +[test_httpssvc_retry_without_ech.js] +skip-if = + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1808048 +run-sequentially = node server exceptions dont replay well +[test_httpssvc_https_upgrade.js] +[test_bug1683176.js] +skip-if = + os == "android" + !debug + (os == "win" && socketprocess_networking) +[test_SuperfluousAuth.js] +[test_trr_confirmation.js] +skip-if = + socketprocess_networking # confirmation state isn't passed cross-process + appname == 'thunderbird' # bug 1760097 +run-sequentially = node server exceptions dont replay well +[test_trr_cancel.js] +run-sequentially = node server exceptions dont replay well +[test_http_server_timing.js] +[test_dns_retry.js] +skip-if = + os == 'mac' # server on a local ipv6 is not started on mac + socketprocess_networking # bug 1760106 +run-sequentially = node server exceptions dont replay well +[test_http3_version1.js] +skip-if = + os == 'win' + os == 'android' +run-sequentially = very high failure rate in parallel +[test_trr_domain.js] +[test_progress_no_proxy_and_proxy.js] +skip-if = + os == 'win' + os == 'android' +run-sequentially = very high failure rate in parallel +[test_http3_0rtt.js] +skip-if = + os == 'win' + os == 'android' +[test_http3_large_post_telemetry.js] +disabled = bug 1771744 - telemetry probe expired +# skip-if = +# asan +# tsan +# os == 'win' +# os == 'android' +# socketprocess_networking +[test_http3_coalescing.js] +skip-if = + os == 'android' + socketprocess_networking + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049 +run-sequentially = node server exceptions dont replay well +[test_websocket_with_h3_active.js] +skip-if = + os == 'android' + verify && (os == 'win') + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049 +run-sequentially = node server exceptions dont replay well +[test_304_headers.js] +[test_http3_direct_proxy.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049 +run-sequentially = node server exceptions dont replay well +[test_bug1725766.js] +skip-if = os == "android" # skip because of bug 1589327 +[test_trr_af_fallback.js] +[test_https_rr_ech_prefs.js] +skip-if = os == "android" +run-sequentially = node server exceptions dont replay well +[test_proxy_pac.js] +[test_trr_enterprise_policy.js] +firefox-appdir = browser # needed for resource:///modules/policies/schema.sys.mjs to be registered +skip-if = + os == "android" + socketprocess_networking +[test_early_hint_listener.js] +skip-if = + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931 +[test_trr_with_proxy.js] +head = head_channels.js head_cache.js head_cookies.js head_trr.js trr_common.js +skip-if = + os == "android" + socketprocess_networking # Bug 1808233 +run-sequentially = node server exceptions dont replay well +[test_trr_blocklist.js] +run-sequentially = node server exceptions dont replay well +[test_https_rr_sorted_alpn.js] +skip-if = os == "android" +run-sequentially = node server exceptions dont replay well +[test_trr_strict_mode.js] +[test_servers.js] +[test_networking_over_socket_process.js] +skip-if = + os == "android" + !socketprocess_networking +run-sequentially = node server exceptions dont replay well +[test_http_408_retry.js] +[test_brotli_decoding.js] +[test_retry_0rtt.js] +skip-if = + verify && (os == 'android') + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1808048 +run-sequentially = tlsserver uses fixed port +[test_http2-proxy-failing.js] +run-sequentially = node server exceptions dont replay well +[test_tls13_disabled.js] +skip-if = + os == 'android' + verify && (os == 'win') + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049 +run-sequentially = node server exceptions dont replay well +[test_proxy-slow-upload.js] +[test_cert_verification_failure.js] +run-sequentially = node server exceptions dont replay well +[test_cert_info.js] +[test_websocket_server.js] +run-sequentially = node server exceptions dont replay well +[test_h2proxy_connection_limit.js] +run-sequentially = node server exceptions dont replay well +[test_pac_reload_after_network_change.js] +[test_proxy_cancel.js] +run-sequentially = node server exceptions dont replay well +[test_connection_based_auth.js] +[test_webtransport_simple.js] +# This test will be fixed in bug 1796556 +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807931 + verify && (os == 'win') + socketprocess_networking +[test_websocket_fails.js] +run-sequentially = node server exceptions dont replay well +skip-if = + (os == "android") && verify # Bug 1804101 +[test_websocket_fails_2.js] +run-sequentially = node server exceptions dont replay well +[test_websocket_server_multiclient.js] +run-sequentially = node server exceptions dont replay well +[test_orb_empty_header.js] +[test_http2_with_proxy.js] +run-sequentially = node server exceptions dont replay well +head = head_channels.js head_cache.js head_cookies.js head_trr.js head_http3.js http2_test_common.js head_servers.js +[test_coaleasing_h2_and_h3_connection.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049 +run-sequentially = http3server +[test_bhttp.js] +[test_oblivious_http.js] +[test_ohttp.js] +[test_websocket_500k.js] +skip-if = verify +run-sequentially = node server exceptions dont replay well +[test_trr_noPrefetch.js] +[test_http3_server.js] +skip-if = + verify + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1808049 +run-sequentially = node server exceptions dont replay well +[test_trr_telemetry.js] +head = head_channels.js head_cache.js head_cookies.js head_trr.js head_http3.js trr_common.js +skip-if = + os == 'android' + socketprocess_networking +[test_trr_proxy_auth.js] +skip-if = + os == 'android' + socketprocess_networking +[test_http3_dns_retry.js] +skip-if = + os == 'android' + os == 'win' && msix + true +run-sequentially = node server exceptions dont replay well diff --git a/netwerk/test/unit_ipc/child_channel_id.js b/netwerk/test/unit_ipc/child_channel_id.js new file mode 100644 index 0000000000..1c9f948151 --- /dev/null +++ b/netwerk/test/unit_ipc/child_channel_id.js @@ -0,0 +1,47 @@ +/** + * Send HTTP requests and notify the parent about their channelId + */ +/* global NetUtil, ChannelListener */ + +let shouldQuit = false; + +function run_test() { + // keep the event loop busy and the test alive until a "finish" command + // is issued by parent + do_timeout(100, function keepAlive() { + if (!shouldQuit) { + do_timeout(100, keepAlive); + } + }); +} + +function makeRequest(uri) { + let requestChannel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + requestChannel.asyncOpen(new ChannelListener(checkResponse, requestChannel)); + requestChannel.QueryInterface(Ci.nsIHttpChannel); + dump(`Child opened request: ${uri}, channelId=${requestChannel.channelId}\n`); +} + +function checkResponse(request, buffer, requestChannel) { + // notify the parent process about the original request channel + requestChannel.QueryInterface(Ci.nsIHttpChannel); + do_send_remote_message(`request:${requestChannel.channelId}`); + + // the response channel can be different (if it was redirected) + let responseChannel = request.QueryInterface(Ci.nsIHttpChannel); + + let uri = responseChannel.URI.spec; + let origUri = responseChannel.originalURI.spec; + let id = responseChannel.channelId; + dump(`Child got response to: ${uri} (orig=${origUri}), channelId=${id}\n`); + + // notify the parent process about this channel's ID + do_send_remote_message(`response:${id}`); +} + +function finish() { + shouldQuit = true; +} diff --git a/netwerk/test/unit_ipc/child_cookie_header.js b/netwerk/test/unit_ipc/child_cookie_header.js new file mode 100644 index 0000000000..4686f509f4 --- /dev/null +++ b/netwerk/test/unit_ipc/child_cookie_header.js @@ -0,0 +1,93 @@ +/* global NetUtil, ChannelListener */ + +"use strict"; + +function inChildProcess() { + return ( + // eslint-disable-next-line mozilla/use-services + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) + .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +let uri = null; +function makeChan() { + return NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function OpenChannelPromise(aChannel, aClosure) { + return new Promise(resolve => { + function processResponse(request, buffer, context) { + aClosure(request.QueryInterface(Ci.nsIHttpChannel), buffer, context); + resolve(); + } + aChannel.asyncOpen(new ChannelListener(processResponse, null)); + }); +} + +// This test doesn't do much, except to communicate with the parent, and get +// URL we need to connect to. +add_task(async function setup() { + ok(inChildProcess(), "Sanity check. This should run in the child process"); + // Initialize the URL. Parent runs the server + do_send_remote_message("start-test"); + uri = await do_await_remote_message("start-test-done"); +}); + +// This test performs a request, and checks that no cookie header are visible +// to the child process +add_task(async function test1() { + let chan = makeChan(); + + await OpenChannelPromise(chan, (request, buffer) => { + equal(buffer, "response"); + Assert.throws( + () => request.getRequestHeader("Cookie"), + /NS_ERROR_NOT_AVAILABLE/, + "Cookie header should not be visible on request in the child" + ); + Assert.throws( + () => request.getResponseHeader("Set-Cookie"), + /NS_ERROR_NOT_AVAILABLE/, + "Cookie header should not be visible on response in the child" + ); + }); + + // We also check that a cookie was saved by the Set-Cookie header + // in the parent. + do_send_remote_message("check-cookie-count"); + let count = await do_await_remote_message("check-cookie-count-done"); + equal(count, 1); +}); + +// This test communicates with the parent, to locally save a new cookie. +// Then it performs another request, makes sure no cookie headers are visible, +// after which it checks that both cookies are visible to the parent. +add_task(async function test2() { + do_send_remote_message("set-cookie"); + await do_await_remote_message("set-cookie-done"); + + let chan = makeChan(); + await OpenChannelPromise(chan, (request, buffer) => { + equal(buffer, "response"); + Assert.throws( + () => request.getRequestHeader("Cookie"), + /NS_ERROR_NOT_AVAILABLE/, + "Cookie header should not be visible on request in the child" + ); + Assert.throws( + () => request.getResponseHeader("Set-Cookie"), + /NS_ERROR_NOT_AVAILABLE/, + "Cookie header should not be visible on response in the child" + ); + }); + + // We should have two cookies. One set by the Set-Cookie header sent by the + // server, and one that was manually set in the parent. + do_send_remote_message("second-check-cookie-count"); + let count = await do_await_remote_message("second-check-cookie-count-done"); + equal(count, 2); +}); diff --git a/netwerk/test/unit_ipc/child_dns_by_type_resolve.js b/netwerk/test/unit_ipc/child_dns_by_type_resolve.js new file mode 100644 index 0000000000..d09d29a73e --- /dev/null +++ b/netwerk/test/unit_ipc/child_dns_by_type_resolve.js @@ -0,0 +1,22 @@ +/* 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"; + +/* import-globals-from ../unit/head_trr.js */ + +let test_answer = "bXkgdm9pY2UgaXMgbXkgcGFzc3dvcmQ="; +let test_answer_addr = "127.0.0.1"; + +add_task(async function testTXTResolve() { + // use the h2 server as DOH provider + let { inRecord, inStatus } = await new TRRDNSListener("_esni.example.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_TXT, + }); + Assert.equal(inStatus, Cr.NS_OK, "status OK"); + let answer = inRecord + .QueryInterface(Ci.nsIDNSTXTRecord) + .getRecordsAsOneString(); + Assert.equal(answer, test_answer, "got correct answer"); +}); diff --git a/netwerk/test/unit_ipc/child_is_proxy_used.js b/netwerk/test/unit_ipc/child_is_proxy_used.js new file mode 100644 index 0000000000..216963ec19 --- /dev/null +++ b/netwerk/test/unit_ipc/child_is_proxy_used.js @@ -0,0 +1,24 @@ +"use strict"; + +/* global NetUtil, ChannelListener, CL_ALLOW_UNKNOWN_CL */ + +add_task(async function check_proxy() { + do_send_remote_message("start-test"); + let URL = await do_await_remote_message("start-test-done"); + let chan = NetUtil.newChannel({ + uri: URL, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + + let { req, buff } = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + equal(buff, "content"); + equal(req.QueryInterface(Ci.nsIHttpChannelInternal).isProxyUsed, true); +}); + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + chan.asyncOpen( + new ChannelListener((req, buff) => resolve({ req, buff }), null, flags) + ); + }); +} diff --git a/netwerk/test/unit_ipc/child_veto_in_parent.js b/netwerk/test/unit_ipc/child_veto_in_parent.js new file mode 100644 index 0000000000..89a614e979 --- /dev/null +++ b/netwerk/test/unit_ipc/child_veto_in_parent.js @@ -0,0 +1,51 @@ +/* import-globals-from ../unit/head_trr.js */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function () { + return URL + randomPath; +}); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", URL + "/content", false); +} + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +add_task(async function doStuff() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(randomPath, redirectHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + let chan = make_channel(randomURI); + let [req, buff] = await new Promise(resolve => + chan.asyncOpen( + new ChannelListener((aReq, aBuff) => resolve([aReq, aBuff]), null) + ) + ); + Assert.equal(buff, ""); + Assert.equal(req.status, Cr.NS_OK); + await httpServer.stop(); + await do_send_remote_message("child-test-done"); +}); diff --git a/netwerk/test/unit_ipc/head_channels_clone.js b/netwerk/test/unit_ipc/head_channels_clone.js new file mode 100644 index 0000000000..f287c6f76f --- /dev/null +++ b/netwerk/test/unit_ipc/head_channels_clone.js @@ -0,0 +1,10 @@ +/* import-globals-from ../unit/head_channels.js */ +// Load standard base class for network tests into child process +// + +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +load("../unit/head_channels.js"); diff --git a/netwerk/test/unit_ipc/head_http3_clone.js b/netwerk/test/unit_ipc/head_http3_clone.js new file mode 100644 index 0000000000..4b4e1155ef --- /dev/null +++ b/netwerk/test/unit_ipc/head_http3_clone.js @@ -0,0 +1,8 @@ +/* import-globals-from ../unit/head_http3.js */ + +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +load("../unit/head_http3.js"); diff --git a/netwerk/test/unit_ipc/head_trr_clone.js b/netwerk/test/unit_ipc/head_trr_clone.js new file mode 100644 index 0000000000..e52c380f19 --- /dev/null +++ b/netwerk/test/unit_ipc/head_trr_clone.js @@ -0,0 +1,8 @@ +/* import-globals-from ../unit/head_trr.js */ + +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +load("../unit/head_trr.js"); diff --git a/netwerk/test/unit_ipc/test_XHR_redirects.js b/netwerk/test/unit_ipc/test_XHR_redirects.js new file mode 100644 index 0000000000..472817a9ec --- /dev/null +++ b/netwerk/test/unit_ipc/test_XHR_redirects.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_XHR_redirects.js"); +} diff --git a/netwerk/test/unit_ipc/test_alt-data_closeWithStatus_wrap.js b/netwerk/test/unit_ipc/test_alt-data_closeWithStatus_wrap.js new file mode 100644 index 0000000000..a9d45a3f12 --- /dev/null +++ b/netwerk/test/unit_ipc/test_alt-data_closeWithStatus_wrap.js @@ -0,0 +1,17 @@ +// needs to be rooted +var cacheFlushObserver = { + observe() { + cacheFlushObserver = null; + do_send_remote_message("flushed"); + }, +}; + +function run_test() { + do_get_profile(); + do_await_remote_message("flush").then(() => { + Services.cache2 + .QueryInterface(Ci.nsICacheTesting) + .flush(cacheFlushObserver); + }); + run_test_in_child("../unit/test_alt-data_closeWithStatus.js"); +} diff --git a/netwerk/test/unit_ipc/test_alt-data_cross_process_wrap.js b/netwerk/test/unit_ipc/test_alt-data_cross_process_wrap.js new file mode 100644 index 0000000000..336b9e3129 --- /dev/null +++ b/netwerk/test/unit_ipc/test_alt-data_cross_process_wrap.js @@ -0,0 +1,96 @@ +// needs to be rooted +var cacheFlushObserver = { + observe() { + cacheFlushObserver = null; + do_send_remote_message("flushed"); + }, +}; + +// We get this from the child a bit later +var url = null; + +// needs to be rooted +var cacheFlushObserver2 = { + observe() { + cacheFlushObserver2 = null; + openAltChannel(); + }, +}; + +function run_test() { + do_get_profile(); + do_await_remote_message("flush").then(() => { + Services.cache2 + .QueryInterface(Ci.nsICacheTesting) + .flush(cacheFlushObserver); + }); + + do_await_remote_message("done").then(() => { + sendCommand("URL;", load_channel); + }); + + run_test_in_child("../unit/test_alt-data_cross_process.js"); +} + +function load_channel(channelUrl) { + ok(channelUrl); + url = channelUrl; // save this to open the alt data channel later + var chan = make_channel(channelUrl); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType("text/binary", "", Ci.nsICacheInfoChannel.ASYNC); + chan.asyncOpen(new ChannelListener(readTextData, null)); +} + +function make_channel(channelUrl, callback, ctx) { + return NetUtil.newChannel({ + uri: channelUrl, + loadUsingSystemPrincipal: true, + }); +} + +function readTextData(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + // Since we are in a different process from what that generated the alt-data, + // we should receive the original data, not processed content. + Assert.equal(cc.alternativeDataType, ""); + Assert.equal(buffer, "response body"); + + // Now let's generate some alt-data in the parent, and make sure we can get it + var altContent = "altContentParentGenerated"; + executeSoon(() => { + var os = cc.openAlternativeOutputStream( + "text/parent-binary", + altContent.length + ); + os.write(altContent, altContent.length); + os.close(); + + executeSoon(() => { + Services.cache2 + .QueryInterface(Ci.nsICacheTesting) + .flush(cacheFlushObserver2); + }); + }); +} + +function openAltChannel() { + var chan = make_channel(url); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType( + "text/parent-binary", + "", + Ci.nsICacheInfoChannel.ASYNC + ); + chan.asyncOpen(new ChannelListener(readAltData, null)); +} + +function readAltData(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + // This was generated in the parent, so it's OK to get it. + Assert.equal(buffer, "altContentParentGenerated"); + Assert.equal(cc.alternativeDataType, "text/parent-binary"); + + // FINISH + do_send_remote_message("finish"); +} diff --git a/netwerk/test/unit_ipc/test_alt-data_simple_wrap.js b/netwerk/test/unit_ipc/test_alt-data_simple_wrap.js new file mode 100644 index 0000000000..ae4f12fde9 --- /dev/null +++ b/netwerk/test/unit_ipc/test_alt-data_simple_wrap.js @@ -0,0 +1,17 @@ +// needs to be rooted +var cacheFlushObserver = { + observe() { + cacheFlushObserver = null; + do_send_remote_message("flushed"); + }, +}; + +function run_test() { + do_get_profile(); + do_await_remote_message("flush").then(() => { + Services.cache2 + .QueryInterface(Ci.nsICacheTesting) + .flush(cacheFlushObserver); + }); + run_test_in_child("../unit/test_alt-data_simple.js"); +} diff --git a/netwerk/test/unit_ipc/test_alt-data_stream_wrap.js b/netwerk/test/unit_ipc/test_alt-data_stream_wrap.js new file mode 100644 index 0000000000..1eee2f2434 --- /dev/null +++ b/netwerk/test/unit_ipc/test_alt-data_stream_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_alt-data_stream.js"); +} diff --git a/netwerk/test/unit_ipc/test_cache-entry-id_wrap.js b/netwerk/test/unit_ipc/test_cache-entry-id_wrap.js new file mode 100644 index 0000000000..0358fc1e8d --- /dev/null +++ b/netwerk/test/unit_ipc/test_cache-entry-id_wrap.js @@ -0,0 +1,13 @@ +function run_test() { + do_get_profile(); + run_test_in_child("../unit/test_cache-entry-id.js"); + + do_await_remote_message("flush").then(() => { + let p = new Promise(resolve => { + Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(resolve); + }); + p.then(() => { + do_send_remote_message("flushed"); + }); + }); +} diff --git a/netwerk/test/unit_ipc/test_cache_jar_wrap.js b/netwerk/test/unit_ipc/test_cache_jar_wrap.js new file mode 100644 index 0000000000..fa2bb82a8d --- /dev/null +++ b/netwerk/test/unit_ipc/test_cache_jar_wrap.js @@ -0,0 +1,4 @@ +function run_test() { + run_test_in_child("../unit/head_cache2.js"); + run_test_in_child("../unit/test_cache_jar.js"); +} diff --git a/netwerk/test/unit_ipc/test_cacheflags_wrap.js b/netwerk/test/unit_ipc/test_cacheflags_wrap.js new file mode 100644 index 0000000000..eda3004532 --- /dev/null +++ b/netwerk/test/unit_ipc/test_cacheflags_wrap.js @@ -0,0 +1,8 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + do_get_profile(); + run_test_in_child("../unit/test_cacheflags.js"); +} diff --git a/netwerk/test/unit_ipc/test_channel_close_wrap.js b/netwerk/test/unit_ipc/test_channel_close_wrap.js new file mode 100644 index 0000000000..ead9999579 --- /dev/null +++ b/netwerk/test/unit_ipc/test_channel_close_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_channel_close.js"); +} diff --git a/netwerk/test/unit_ipc/test_channel_id.js b/netwerk/test/unit_ipc/test_channel_id.js new file mode 100644 index 0000000000..01599509bb --- /dev/null +++ b/netwerk/test/unit_ipc/test_channel_id.js @@ -0,0 +1,107 @@ +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +/* + * Test that when doing HTTP requests, the nsIHttpChannel is detected in + * both parent and child and shares the same channelId across processes. + */ + +let httpserver; +let port; + +function startHttpServer() { + httpserver = new HttpServer(); + + httpserver.registerPathHandler("/resource", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + response.bodyOutputStream.write("data", 4); + }); + + httpserver.registerPathHandler("/redirect", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 302, "Redirect"); + response.setHeader("Location", "/resource", false); + response.setHeader("Cache-Control", "no-cache", false); + }); + + httpserver.start(-1); + port = httpserver.identity.primaryPort; +} + +function stopHttpServer(next) { + httpserver.stop(next); +} + +let expectedParentChannels = []; +let expectedChildMessages = []; + +let maybeFinishWaitForParentChannels; +let parentChannelsDone = new Promise(resolve => { + maybeFinishWaitForParentChannels = () => { + if (!expectedParentChannels.length) { + dump("All expected parent channels were detected\n"); + resolve(); + } + }; +}); + +function observer(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + + let uri = channel.URI.spec; + let origUri = channel.originalURI.spec; + let id = channel.channelId; + dump(`Parent detected channel: ${uri} (orig=${origUri}): channelId=${id}\n`); + + // did we expect a new channel? + let expected = expectedParentChannels.shift(); + Assert.ok(!!expected); + + // Start waiting for the messages about request/response from child + for (let event of expected) { + let message = `${event}:${id}`; + dump(`Expecting message from child: ${message}\n`); + + let messagePromise = do_await_remote_message(message).then(() => { + dump(`Expected message from child arrived: ${message}\n`); + }); + expectedChildMessages.push(messagePromise); + } + + // If we don't expect any further parent channels, finish the parent wait + maybeFinishWaitForParentChannels(); +} + +function run_test() { + startHttpServer(); + Services.obs.addObserver(observer, "http-on-modify-request"); + run_test_in_child("child_channel_id.js", makeRequests); +} + +function makeRequests() { + // First, a normal request without any redirect. Expect one channel detected + // in parent, used by both request and response. + expectedParentChannels.push(["request", "response"]); + sendCommand(`makeRequest("http://localhost:${port}/resource");`); + + // Second request will be redirected. Expect two channels, one with the + // original request, then the redirected one which gets the final response. + expectedParentChannels.push(["request"], ["response"]); + sendCommand(`makeRequest("http://localhost:${port}/redirect");`); + + waitForParentChannels(); +} + +function waitForParentChannels() { + parentChannelsDone.then(waitForChildMessages); +} + +function waitForChildMessages() { + dump(`Waiting for ${expectedChildMessages.length} child messages\n`); + Promise.all(expectedChildMessages).then(finish); +} + +function finish() { + Services.obs.removeObserver(observer, "http-on-modify-request"); + sendCommand("finish();", () => stopHttpServer(do_test_finished)); +} diff --git a/netwerk/test/unit_ipc/test_channel_priority_wrap.js b/netwerk/test/unit_ipc/test_channel_priority_wrap.js new file mode 100644 index 0000000000..c443d221d0 --- /dev/null +++ b/netwerk/test/unit_ipc/test_channel_priority_wrap.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +let httpserver; +let port; + +function startHttpServer() { + httpserver = new HttpServer(); + + httpserver.registerPathHandler("/resource", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + response.bodyOutputStream.write("data", 4); + }); + + httpserver.registerPathHandler("/redirect", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 302, "Redirect"); + response.setHeader("Location", "/resource", false); + response.setHeader("Cache-Control", "no-cache", false); + }); + + httpserver.start(-1); + port = httpserver.identity.primaryPort; +} + +function stopHttpServer() { + httpserver.stop(() => {}); +} + +function run_test() { + // jshint ignore:line + registerCleanupFunction(stopHttpServer); + + run_test_in_child("../unit/test_channel_priority.js", () => { + startHttpServer(); + sendCommand(`configPort(${port});`); + do_await_remote_message("finished").then(() => { + do_test_finished(); + }); + }); +} diff --git a/netwerk/test/unit_ipc/test_chunked_responses_wrap.js b/netwerk/test/unit_ipc/test_chunked_responses_wrap.js new file mode 100644 index 0000000000..72bb40554c --- /dev/null +++ b/netwerk/test/unit_ipc/test_chunked_responses_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_chunked_responses.js"); +} diff --git a/netwerk/test/unit_ipc/test_cookie_header_stripped.js b/netwerk/test/unit_ipc/test_cookie_header_stripped.js new file mode 100644 index 0000000000..f70a48e4af --- /dev/null +++ b/netwerk/test/unit_ipc/test_cookie_header_stripped.js @@ -0,0 +1,92 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const TEST_DOMAIN = "www.example.com"; +XPCOMUtils.defineLazyGetter(this, "URL", function () { + return ( + "http://" + TEST_DOMAIN + ":" + httpserv.identity.primaryPort + "/path" + ); +}); + +const responseBody1 = "response"; +function requestHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Set-Cookie", "tom=cool; Max-Age=10", true); + response.bodyOutputStream.write(responseBody1, responseBody1.length); +} + +let httpserv = null; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/path", requestHandler); + httpserv.start(-1); + httpserv.identity.add("http", TEST_DOMAIN, httpserv.identity.primaryPort); + + registerCleanupFunction(() => { + Services.cookies.removeCookiesWithOriginAttributes("{}", TEST_DOMAIN); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.cookie.cookieBehavior"); + Services.prefs.clearUserPref( + "network.cookieJarSettings.unblocked_for_testing" + ); + + httpserv.stop(); + httpserv = null; + }); + + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + Services.prefs.setCharPref("network.dns.localDomains", TEST_DOMAIN); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.cookies.removeCookiesWithOriginAttributes("{}", TEST_DOMAIN); + + // Sends back the URL to the child script + do_await_remote_message("start-test").then(() => { + do_send_remote_message("start-test-done", URL); + }); + + // Sends back the cookie count for the domain + // Should only be one - from Set-Cookie + do_await_remote_message("check-cookie-count").then(() => { + do_send_remote_message( + "check-cookie-count-done", + Services.cookies.countCookiesFromHost(TEST_DOMAIN) + ); + }); + + // Sends back the cookie count for the domain + // There should be 2 cookies. One from the Set-Cookie header, the other set + // manually. + do_await_remote_message("second-check-cookie-count").then(() => { + do_send_remote_message( + "second-check-cookie-count-done", + Services.cookies.countCookiesFromHost(TEST_DOMAIN) + ); + }); + + // Sets a cookie for the test domain + do_await_remote_message("set-cookie").then(() => { + const expiry = Date.now() + 24 * 60 * 60; + Services.cookies.add( + TEST_DOMAIN, + "/", + "cookieName", + "cookieValue", + false, + false, + false, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + do_send_remote_message("set-cookie-done"); + }); + + // Run the actual test logic + run_test_in_child("child_cookie_header.js"); +} diff --git a/netwerk/test/unit_ipc/test_cookiejars_wrap.js b/netwerk/test/unit_ipc/test_cookiejars_wrap.js new file mode 100644 index 0000000000..bb58bcd2bf --- /dev/null +++ b/netwerk/test/unit_ipc/test_cookiejars_wrap.js @@ -0,0 +1,13 @@ +function run_test() { + // Allow all cookies. + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + Services.prefs.setBoolPref( + "network.cookie.skip_browsing_context_check_in_parent_for_testing", + true + ); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + run_test_in_child("../unit/test_cookiejars.js"); +} diff --git a/netwerk/test/unit_ipc/test_dns_by_type_resolve_wrap.js b/netwerk/test/unit_ipc/test_dns_by_type_resolve_wrap.js new file mode 100644 index 0000000000..72b8f96880 --- /dev/null +++ b/netwerk/test/unit_ipc/test_dns_by_type_resolve_wrap.js @@ -0,0 +1,52 @@ +"use strict"; + +let h2Port; + +function setup() { + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + + Services.prefs.setBoolPref("network.http.http2.enabled", true); + // the TRR server is on 127.0.0.1 + Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1"); + + // make all native resolve calls "secretly" resolve localhost instead + Services.prefs.setBoolPref("network.dns.native-is-localhost", true); + + // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow + Services.prefs.setBoolPref("network.trr.wait-for-portal", false); + // don't confirm that TRR is working, just go! + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + + // So we can change the pref without clearing the cache to check a pushed + // record with a TRR path that fails. + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + + // XXX(valentin): It would be nice to just call trr_test_setup() here, but + // the relative path here makes it awkward. Would be nice to fix someday. + addCertFromFile(certdb, "../unit/http2-ca.pem", "CTu,u,u"); +} + +setup(); +registerCleanupFunction(() => { + trr_clear_prefs(); +}); + +function run_test() { + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/doh" + ); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR first + run_test_in_child("child_dns_by_type_resolve.js"); +} diff --git a/netwerk/test/unit_ipc/test_dns_cancel_wrap.js b/netwerk/test/unit_ipc/test_dns_cancel_wrap.js new file mode 100644 index 0000000000..2f38aa4ecb --- /dev/null +++ b/netwerk/test/unit_ipc/test_dns_cancel_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_dns_cancel.js"); +} diff --git a/netwerk/test/unit_ipc/test_dns_service_wrap.js b/netwerk/test/unit_ipc/test_dns_service_wrap.js new file mode 100644 index 0000000000..fdbecf16d3 --- /dev/null +++ b/netwerk/test/unit_ipc/test_dns_service_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_dns_service.js"); +} diff --git a/netwerk/test/unit_ipc/test_duplicate_headers_wrap.js b/netwerk/test/unit_ipc/test_duplicate_headers_wrap.js new file mode 100644 index 0000000000..6225d593d9 --- /dev/null +++ b/netwerk/test/unit_ipc/test_duplicate_headers_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_duplicate_headers.js"); +} diff --git a/netwerk/test/unit_ipc/test_event_sink_wrap.js b/netwerk/test/unit_ipc/test_event_sink_wrap.js new file mode 100644 index 0000000000..908c971f8a --- /dev/null +++ b/netwerk/test/unit_ipc/test_event_sink_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_event_sink.js"); +} diff --git a/netwerk/test/unit_ipc/test_getHost_wrap.js b/netwerk/test/unit_ipc/test_getHost_wrap.js new file mode 100644 index 0000000000..f74ab7d152 --- /dev/null +++ b/netwerk/test/unit_ipc/test_getHost_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_getHost.js"); +} diff --git a/netwerk/test/unit_ipc/test_gio_protocol_wrap.js b/netwerk/test/unit_ipc/test_gio_protocol_wrap.js new file mode 100644 index 0000000000..f6aa6b83e8 --- /dev/null +++ b/netwerk/test/unit_ipc/test_gio_protocol_wrap.js @@ -0,0 +1,21 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// +// + +function run_test() { + Services.prefs.setCharPref( + "network.gio.supported-protocols", + "localtest:,recent:" + ); + + do_await_remote_message("gio-allow-test-protocols").then(port => { + do_send_remote_message("gio-allow-test-protocols-done"); + }); + + run_test_in_child("../unit/test_gio_protocol.js"); +} + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.gio.supported-protocols"); +}); diff --git a/netwerk/test/unit_ipc/test_head_wrap.js b/netwerk/test/unit_ipc/test_head_wrap.js new file mode 100644 index 0000000000..13f0702e55 --- /dev/null +++ b/netwerk/test/unit_ipc/test_head_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_head.js"); +} diff --git a/netwerk/test/unit_ipc/test_headers_wrap.js b/netwerk/test/unit_ipc/test_headers_wrap.js new file mode 100644 index 0000000000..e0bae4080d --- /dev/null +++ b/netwerk/test/unit_ipc/test_headers_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_headers.js"); +} diff --git a/netwerk/test/unit_ipc/test_http3_prio_disabled_wrap.js b/netwerk/test/unit_ipc/test_http3_prio_disabled_wrap.js new file mode 100644 index 0000000000..38988cd450 --- /dev/null +++ b/netwerk/test/unit_ipc/test_http3_prio_disabled_wrap.js @@ -0,0 +1,20 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.http3.priority"); + http3_clear_prefs(); +}); + +// setup will be called before the child process tests +add_task(async function setup() { + await http3_setup_tests("h3-29"); +}); + +async function run_test() { + // test priority urgency and incremental with priority disabled + Services.prefs.setBoolPref("network.http.http3.priority", false); + run_test_in_child("../unit/test_http3_prio_disabled.js"); + run_next_test(); // only pumps next async task from this file +} diff --git a/netwerk/test/unit_ipc/test_http3_prio_enabled_wrap.js b/netwerk/test/unit_ipc/test_http3_prio_enabled_wrap.js new file mode 100644 index 0000000000..dcff09fcba --- /dev/null +++ b/netwerk/test/unit_ipc/test_http3_prio_enabled_wrap.js @@ -0,0 +1,20 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.http3.priority"); + http3_clear_prefs(); +}); + +// setup will be called before the child process tests +add_task(async function setup() { + await http3_setup_tests("h3-29"); +}); + +async function run_test() { + // test priority urgency and incremental with priority enabled + Services.prefs.setBoolPref("network.http.http3.priority", true); + run_test_in_child("../unit/test_http3_prio_enabled.js"); + run_next_test(); // only pumps next async task from this file +} diff --git a/netwerk/test/unit_ipc/test_httpcancel_wrap.js b/netwerk/test/unit_ipc/test_httpcancel_wrap.js new file mode 100644 index 0000000000..f56d0e198b --- /dev/null +++ b/netwerk/test/unit_ipc/test_httpcancel_wrap.js @@ -0,0 +1,48 @@ +"use strict"; + +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +let observer = null; + +function run_test() { + do_await_remote_message("register-observer").then(() => { + observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(subject, topic, data) { + subject = subject.QueryInterface(Ci.nsIRequest); + subject.cancel(Cr.NS_BINDING_ABORTED); + + // ENSURE_CALLED_BEFORE_CONNECT: setting values should still work + try { + subject.QueryInterface(Ci.nsIHttpChannel); + let currentReferrer = subject.getRequestHeader("Referer"); + Assert.equal(currentReferrer, "http://site1.com/"); + let uri = Services.io.newURI("http://site2.com"); + subject.referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.EMPTY, + true, + uri + ); + } catch (ex) { + do_throw("Exception: " + ex); + } + }, + }; + + Services.obs.addObserver(observer, "http-on-modify-request"); + do_send_remote_message("register-observer-done"); + }); + + do_await_remote_message("unregister-observer").then(() => { + Services.obs.removeObserver(observer, "http-on-modify-request"); + + do_send_remote_message("unregister-observer-done"); + }); + + run_test_in_child("../unit/test_httpcancel.js"); +} diff --git a/netwerk/test/unit_ipc/test_httpsuspend_wrap.js b/netwerk/test/unit_ipc/test_httpsuspend_wrap.js new file mode 100644 index 0000000000..0b013bd957 --- /dev/null +++ b/netwerk/test/unit_ipc/test_httpsuspend_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_httpsuspend.js"); +} diff --git a/netwerk/test/unit_ipc/test_is_proxy_used.js b/netwerk/test/unit_ipc/test_is_proxy_used.js new file mode 100644 index 0000000000..2678ffa1b8 --- /dev/null +++ b/netwerk/test/unit_ipc/test_is_proxy_used.js @@ -0,0 +1,32 @@ +"use strict"; +/* global NodeHTTPServer, NodeHTTPProxyServer*/ + +add_task(async function run_test() { + let proxy = new NodeHTTPProxyServer(); + await proxy.start(); + registerCleanupFunction(async () => { + await proxy.stop(); + }); + + let server = new NodeHTTPServer(); + + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + + await server.registerPathHandler("/test", (req, resp) => { + let content = "content"; + resp.writeHead(200); + resp.end(content); + }); + + do_await_remote_message("start-test").then(() => { + do_send_remote_message( + "start-test-done", + `http://localhost:${server.port()}/test` + ); + }); + + run_test_in_child("child_is_proxy_used.js"); +}); diff --git a/netwerk/test/unit_ipc/test_multipart_streamconv_wrap.js b/netwerk/test/unit_ipc/test_multipart_streamconv_wrap.js new file mode 100644 index 0000000000..b116f095ba --- /dev/null +++ b/netwerk/test/unit_ipc/test_multipart_streamconv_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_multipart_streamconv.js"); +} diff --git a/netwerk/test/unit_ipc/test_orb_empty_header_wrap.js b/netwerk/test/unit_ipc/test_orb_empty_header_wrap.js new file mode 100644 index 0000000000..dec6c53fee --- /dev/null +++ b/netwerk/test/unit_ipc/test_orb_empty_header_wrap.js @@ -0,0 +1,8 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// +// setup will be called before the child process tests +function run_test() { + Services.prefs.setBoolPref("browser.opaqueResponseBlocking", true); + run_test_in_child("../unit/test_orb_empty_header.js"); +} diff --git a/netwerk/test/unit_ipc/test_original_sent_received_head_wrap.js b/netwerk/test/unit_ipc/test_original_sent_received_head_wrap.js new file mode 100644 index 0000000000..91a8a00f09 --- /dev/null +++ b/netwerk/test/unit_ipc/test_original_sent_received_head_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_original_sent_received_head.js"); +} diff --git a/netwerk/test/unit_ipc/test_post_wrap.js b/netwerk/test/unit_ipc/test_post_wrap.js new file mode 100644 index 0000000000..27afae5b40 --- /dev/null +++ b/netwerk/test/unit_ipc/test_post_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_post.js"); +} diff --git a/netwerk/test/unit_ipc/test_predictor_wrap.js b/netwerk/test/unit_ipc/test_predictor_wrap.js new file mode 100644 index 0000000000..24aafe1a09 --- /dev/null +++ b/netwerk/test/unit_ipc/test_predictor_wrap.js @@ -0,0 +1,44 @@ +// This is the bit that runs in the parent process when the test begins. Here's +// what happens: +// +// - Load the entire single-process test in the parent +// - Setup the test harness within the child process +// - Send a command to the child to have the single-process test loaded there, as well +// - Send a command to the child to make the predictor available +// - run_test_real in the parent +// - run_test_real does a bunch of pref-setting and other fun things on the parent +// - once all that's done, it calls run_next_test IN THE PARENT +// - every time run_next_test is called, it is called IN THE PARENT +// - the test that gets started then does any parent-side setup that's necessary +// - once the parent-side setup is done, it calls continue_<testname> IN THE CHILD +// - when the test is done running on the child, the child calls predictor.reset +// this causes some code to be run on the parent which, when complete, sends an +// obserer service notification IN THE PARENT, which causes run_next_test to be +// called again, bumping us up to the top of the loop outlined here +// - when the final test is done, cleanup happens IN THE PARENT and we're done +// +// This is a little confusing, but it's what we have to do in order to have some +// things that must run on the parent (the setup - opening cache entries, etc) +// but with most of the test running on the child (calls to the predictor api, +// verification, etc). +// +/* import-globals-from ../unit/test_predictor.js */ + +function run_test() { + var test_path = do_get_file("../unit/test_predictor.js").path.replace( + /\\/g, + "/" + ); + load(test_path); + do_load_child_test_harness(); + do_test_pending(); + sendCommand('load("' + test_path + '");', function () { + sendCommand( + 'predictor = Cc["@mozilla.org/network/predictor;1"].getService(Ci.nsINetworkPredictor);', + function () { + run_test_real(); + do_test_finished(); + } + ); + }); +} diff --git a/netwerk/test/unit_ipc/test_progress_wrap.js b/netwerk/test/unit_ipc/test_progress_wrap.js new file mode 100644 index 0000000000..c4a658c094 --- /dev/null +++ b/netwerk/test/unit_ipc/test_progress_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_progress.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect-caching_canceled_wrap.js b/netwerk/test/unit_ipc/test_redirect-caching_canceled_wrap.js new file mode 100644 index 0000000000..8a12f4090e --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect-caching_canceled_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// +// +function run_test() { + run_test_in_child("../unit/test_redirect-caching_canceled.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect-caching_failure_wrap.js b/netwerk/test/unit_ipc/test_redirect-caching_failure_wrap.js new file mode 100644 index 0000000000..f95cc399be --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect-caching_failure_wrap.js @@ -0,0 +1,11 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// +// +function run_test() { + do_await_remote_message("disable-ports").then(_ => { + Services.prefs.setCharPref("network.security.ports.banned", "65400"); + do_send_remote_message("disable-ports-done"); + }); + run_test_in_child("../unit/test_redirect-caching_failure.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect-caching_passing_wrap.js b/netwerk/test/unit_ipc/test_redirect-caching_passing_wrap.js new file mode 100644 index 0000000000..6288bc311a --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect-caching_passing_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// +// +function run_test() { + run_test_in_child("../unit/test_redirect-caching_passing.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_canceled_wrap.js b/netwerk/test/unit_ipc/test_redirect_canceled_wrap.js new file mode 100644 index 0000000000..53fa9a5cfc --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_canceled_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// +// +function run_test() { + run_test_in_child("../unit/test_redirect_canceled.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_different-protocol_wrap.js b/netwerk/test/unit_ipc/test_redirect_different-protocol_wrap.js new file mode 100644 index 0000000000..c45e7810ab --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_different-protocol_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_redirect_different-protocol.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_failure_wrap.js b/netwerk/test/unit_ipc/test_redirect_failure_wrap.js new file mode 100644 index 0000000000..ac935afe78 --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_failure_wrap.js @@ -0,0 +1,8 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + Services.prefs.setCharPref("network.security.ports.banned", "65400"); + run_test_in_child("../unit/test_redirect_failure.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_from_script_wrap.js b/netwerk/test/unit_ipc/test_redirect_from_script_wrap.js new file mode 100644 index 0000000000..74f77a9ea1 --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_from_script_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_redirect_from_script.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_history_wrap.js b/netwerk/test/unit_ipc/test_redirect_history_wrap.js new file mode 100644 index 0000000000..38cdfa35ec --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_history_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_redirect_history.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_passing_wrap.js b/netwerk/test/unit_ipc/test_redirect_passing_wrap.js new file mode 100644 index 0000000000..597ac35fb4 --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_passing_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_redirect_passing.js"); +} diff --git a/netwerk/test/unit_ipc/test_redirect_veto_parent.js b/netwerk/test/unit_ipc/test_redirect_veto_parent.js new file mode 100644 index 0000000000..c2fa3fa000 --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_veto_parent.js @@ -0,0 +1,64 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// +// + +let ChannelEventSink2 = { + _classDescription: "WebRequest channel event sink", + _classID: Components.ID("115062f8-92f1-11e5-8b7f-08001110f7ec"), + _contractID: "@mozilla.org/webrequest/channel-event-sink;1", + + QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink", "nsIFactory"]), + + init() { + Components.manager + .QueryInterface(Ci.nsIComponentRegistrar) + .registerFactory( + this._classID, + this._classDescription, + this._contractID, + this + ); + }, + + register() { + Services.catMan.addCategoryEntry( + "net-channel-event-sinks", + this._contractID, + this._contractID, + false, + true + ); + }, + + unregister() { + Components.manager + .QueryInterface(Ci.nsIComponentRegistrar) + .unregisterFactory(this._classID, ChannelEventSink2); + Services.catMan.deleteCategoryEntry( + "net-channel-event-sinks", + this._contractID, + false + ); + }, + + // nsIChannelEventSink implementation + asyncOnChannelRedirect(oldChannel, newChannel, flags, redirectCallback) { + // Abort the redirection + redirectCallback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE); + }, + + // nsIFactory implementation + createInstance(iid) { + return this.QueryInterface(iid); + }, +}; + +add_task(async function run_test() { + ChannelEventSink2.init(); + ChannelEventSink2.register(); + + run_test_in_child("child_veto_in_parent.js"); + await do_await_remote_message("child-test-done"); + ChannelEventSink2.unregister(); +}); diff --git a/netwerk/test/unit_ipc/test_redirect_veto_wrap.js b/netwerk/test/unit_ipc/test_redirect_veto_wrap.js new file mode 100644 index 0000000000..554683d944 --- /dev/null +++ b/netwerk/test/unit_ipc/test_redirect_veto_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// +// +function run_test() { + run_test_in_child("../unit/test_redirect_veto.js"); +} diff --git a/netwerk/test/unit_ipc/test_reentrancy_wrap.js b/netwerk/test/unit_ipc/test_reentrancy_wrap.js new file mode 100644 index 0000000000..18f2509159 --- /dev/null +++ b/netwerk/test/unit_ipc/test_reentrancy_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_reentrancy.js"); +} diff --git a/netwerk/test/unit_ipc/test_reply_without_content_type_wrap.js b/netwerk/test/unit_ipc/test_reply_without_content_type_wrap.js new file mode 100644 index 0000000000..f2d90c33d0 --- /dev/null +++ b/netwerk/test/unit_ipc/test_reply_without_content_type_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_reply_without_content_type.js"); +} diff --git a/netwerk/test/unit_ipc/test_resumable_channel_wrap.js b/netwerk/test/unit_ipc/test_resumable_channel_wrap.js new file mode 100644 index 0000000000..573ab25b64 --- /dev/null +++ b/netwerk/test/unit_ipc/test_resumable_channel_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_resumable_channel.js"); +} diff --git a/netwerk/test/unit_ipc/test_simple_wrap.js b/netwerk/test/unit_ipc/test_simple_wrap.js new file mode 100644 index 0000000000..8c6957e944 --- /dev/null +++ b/netwerk/test/unit_ipc/test_simple_wrap.js @@ -0,0 +1,7 @@ +// +// Run test script in content process instead of chrome (xpcshell's default) +// + +function run_test() { + run_test_in_child("../unit/test_simple.js"); +} diff --git a/netwerk/test/unit_ipc/test_trackingProtection_annotateChannels_wrap1.js b/netwerk/test/unit_ipc/test_trackingProtection_annotateChannels_wrap1.js new file mode 100644 index 0000000000..efe6978f35 --- /dev/null +++ b/netwerk/test/unit_ipc/test_trackingProtection_annotateChannels_wrap1.js @@ -0,0 +1,26 @@ +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); + +function run_test() { + do_get_profile(); + Services.prefs.setBoolPref( + "privacy.trackingprotection.annotate_channels", + false + ); + Services.prefs.setBoolPref( + "privacy.trackingprotection.lower_network_priority", + false + ); + do_test_pending(); + UrlClassifierTestUtils.addTestTrackers().then(() => { + run_test_in_child( + "../unit/test_trackingProtection_annotateChannels.js", + () => { + UrlClassifierTestUtils.cleanupTestTrackers(); + do_test_finished(); + } + ); + do_test_finished(); + }); +} diff --git a/netwerk/test/unit_ipc/test_trackingProtection_annotateChannels_wrap2.js b/netwerk/test/unit_ipc/test_trackingProtection_annotateChannels_wrap2.js new file mode 100644 index 0000000000..ab5eb356fd --- /dev/null +++ b/netwerk/test/unit_ipc/test_trackingProtection_annotateChannels_wrap2.js @@ -0,0 +1,26 @@ +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); + +function run_test() { + do_get_profile(); + Services.prefs.setBoolPref( + "privacy.trackingprotection.annotate_channels", + true + ); + Services.prefs.setBoolPref( + "privacy.trackingprotection.lower_network_priority", + true + ); + do_test_pending(); + UrlClassifierTestUtils.addTestTrackers().then(() => { + run_test_in_child( + "../unit/test_trackingProtection_annotateChannels.js", + () => { + UrlClassifierTestUtils.cleanupTestTrackers(); + do_test_finished(); + } + ); + do_test_finished(); + }); +} diff --git a/netwerk/test/unit_ipc/test_trr_httpssvc_wrap.js b/netwerk/test/unit_ipc/test_trr_httpssvc_wrap.js new file mode 100644 index 0000000000..11d933a71a --- /dev/null +++ b/netwerk/test/unit_ipc/test_trr_httpssvc_wrap.js @@ -0,0 +1,88 @@ +"use strict"; + +let h2Port; +let prefs; + +function setup() { + h2Port = Services.env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Services.prefs; + + prefs.setBoolPref("network.security.esni.enabled", false); + prefs.setBoolPref("network.http.http2.enabled", true); + // the TRR server is on 127.0.0.1 + prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1"); + + // make all native resolve calls "secretly" resolve localhost instead + prefs.setBoolPref("network.dns.native-is-localhost", true); + + // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow + prefs.setIntPref("network.trr.mode", 3); // TRR first + prefs.setBoolPref("network.trr.wait-for-portal", false); + // don't confirm that TRR is working, just go! + prefs.setCharPref("network.trr.confirmationNS", "skip"); + + // So we can change the pref without clearing the cache to check a pushed + // record with a TRR path that fails. + prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "../unit/http2-ca.pem", "CTu,u,u"); +} + +setup(); +registerCleanupFunction(() => { + prefs.clearUserPref("network.security.esni.enabled"); + prefs.clearUserPref("network.http.http2.enabled"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.dns.native-is-localhost"); + prefs.clearUserPref("network.trr.mode"); + prefs.clearUserPref("network.trr.uri"); + prefs.clearUserPref("network.trr.credentials"); + prefs.clearUserPref("network.trr.wait-for-portal"); + prefs.clearUserPref("network.trr.allow-rfc1918"); + prefs.clearUserPref("network.trr.useGET"); + prefs.clearUserPref("network.trr.confirmationNS"); + prefs.clearUserPref("network.trr.bootstrapAddr"); + prefs.clearUserPref("network.trr.temp_blocklist_duration_sec"); + prefs.clearUserPref("network.trr.request-timeout"); + prefs.clearUserPref("network.trr.clear-cache-on-pref-change"); + prefs.clearUserPref("network.dns.port_prefixed_qname_https_rr"); +}); + +function run_test() { + prefs.setIntPref("network.trr.mode", 3); + prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/httpssvc" + ); + + do_await_remote_message("mode3-port").then(port => { + prefs.setIntPref("network.trr.mode", 3); + prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${port}/dns-query` + ); + do_send_remote_message("mode3-port-done"); + }); + + do_await_remote_message("clearCache").then(() => { + Services.dns.clearCache(true); + do_send_remote_message("clearCache-done"); + }); + + do_await_remote_message("set-port-prefixed-pref").then(() => { + prefs.setBoolPref("network.dns.port_prefixed_qname_https_rr", true); + do_send_remote_message("set-port-prefixed-pref-done"); + }); + + run_test_in_child("../unit/test_trr_httpssvc.js"); +} diff --git a/netwerk/test/unit_ipc/test_xmlhttprequest_wrap.js b/netwerk/test/unit_ipc/test_xmlhttprequest_wrap.js new file mode 100644 index 0000000000..00b4a0b85f --- /dev/null +++ b/netwerk/test/unit_ipc/test_xmlhttprequest_wrap.js @@ -0,0 +1,3 @@ +function run_test() { + run_test_in_child("../unit/test_xmlhttprequest.js"); +} diff --git a/netwerk/test/unit_ipc/xpcshell.ini b/netwerk/test/unit_ipc/xpcshell.ini new file mode 100644 index 0000000000..2d22a5a134 --- /dev/null +++ b/netwerk/test/unit_ipc/xpcshell.ini @@ -0,0 +1,186 @@ +[DEFAULT] +head = head_channels_clone.js head_trr_clone.js head_http3_clone.js ../unit/head_servers.js +skip-if = socketprocess_networking +# Several tests rely on redirecting to data: URIs, which was allowed for a long +# time but now forbidden. So we enable it just for these tests. +prefs = + network.allow_redirect_to_data=true +support-files = + child_channel_id.js + !/netwerk/test/unit/test_XHR_redirects.js + !/netwerk/test/unit/test_bug528292.js + !/netwerk/test/unit/test_cache-entry-id.js + !/netwerk/test/unit/test_cache_jar.js + !/netwerk/test/unit/test_cacheflags.js + !/netwerk/test/unit/test_channel_close.js + !/netwerk/test/unit/test_cookiejars.js + !/netwerk/test/unit/test_dns_cancel.js + !/netwerk/test/unit/test_dns_service.js + !/netwerk/test/unit/test_duplicate_headers.js + !/netwerk/test/unit/test_event_sink.js + !/netwerk/test/unit/test_getHost.js + !/netwerk/test/unit/test_gio_protocol.js + !/netwerk/test/unit/test_head.js + !/netwerk/test/unit/test_headers.js + !/netwerk/test/unit/test_httpsuspend.js + !/netwerk/test/unit/test_post.js + !/netwerk/test/unit/test_predictor.js + !/netwerk/test/unit/test_progress.js + !/netwerk/test/unit/test_redirect_veto.js + !/netwerk/test/unit/test_redirect-caching_canceled.js + !/netwerk/test/unit/test_redirect-caching_failure.js + !/netwerk/test/unit/test_redirect-caching_passing.js + !/netwerk/test/unit/test_redirect_canceled.js + !/netwerk/test/unit/test_redirect_different-protocol.js + !/netwerk/test/unit/test_redirect_failure.js + !/netwerk/test/unit/test_redirect_from_script.js + !/netwerk/test/unit/test_redirect_history.js + !/netwerk/test/unit/test_redirect_passing.js + !/netwerk/test/unit/test_reentrancy.js + !/netwerk/test/unit/test_reply_without_content_type.js + !/netwerk/test/unit/test_resumable_channel.js + !/netwerk/test/unit/test_simple.js + !/netwerk/test/unit/test_trackingProtection_annotateChannels.js + !/netwerk/test/unit/test_xmlhttprequest.js + !/netwerk/test/unit/head_channels.js + !/netwerk/test/unit/head_trr.js + !/netwerk/test/unit/head_cache2.js + !/netwerk/test/unit/data/image.png + !/netwerk/test/unit/data/system_root.lnk + !/netwerk/test/unit/data/test_psl.txt + !/netwerk/test/unit/data/test_readline1.txt + !/netwerk/test/unit/data/test_readline2.txt + !/netwerk/test/unit/data/test_readline3.txt + !/netwerk/test/unit/data/test_readline4.txt + !/netwerk/test/unit/data/test_readline5.txt + !/netwerk/test/unit/data/test_readline6.txt + !/netwerk/test/unit/data/test_readline7.txt + !/netwerk/test/unit/data/test_readline8.txt + !/netwerk/test/unit/data/signed_win.exe + !/netwerk/test/unit/test_alt-data_simple.js + !/netwerk/test/unit/test_alt-data_stream.js + !/netwerk/test/unit/test_alt-data_closeWithStatus.js + !/netwerk/test/unit/test_channel_priority.js + !/netwerk/test/unit/test_multipart_streamconv.js + !/netwerk/test/unit/test_original_sent_received_head.js + !/netwerk/test/unit/test_alt-data_cross_process.js + !/netwerk/test/unit/test_httpcancel.js + !/netwerk/test/unit/test_trr_httpssvc.js + !/netwerk/test/unit/test_http3_prio_enabled.js + !/netwerk/test/unit/test_http3_prio_disabled.js + !/netwerk/test/unit/test_http3_prio_helpers.js + !/netwerk/test/unit/http2-ca.pem + !/netwerk/test/unit/test_orb_empty_header.js + child_is_proxy_used.js + child_cookie_header.js + child_dns_by_type_resolve.js + child_veto_in_parent.js + + +[test_cookie_header_stripped.js] +[test_cacheflags_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_cache-entry-id_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_cache_jar_wrap.js] +[test_channel_close_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_chunked_responses_wrap.js] +prefs = + network.allow_raw_sockets_in_content_processes=true + security.allow_eval_with_system_principal=true +[test_cookiejars_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_dns_cancel_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_dns_service_wrap.js] +[test_duplicate_headers_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_event_sink_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_gio_protocol_wrap.js] +skip-if = (toolkit != 'gtk') +[test_head_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_headers_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_httpsuspend_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_post_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_predictor_wrap.js] +[test_progress_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_redirect-caching_canceled_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_redirect-caching_failure_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_redirect-caching_passing_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_redirect_canceled_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_redirect_failure_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +# Do not test the channel.redirectTo() API under e10s until 827269 is resolved +[test_redirect_from_script_wrap.js] +skip-if = true +[test_redirect_veto_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_redirect_veto_parent.js] +prefs = network.allow_raw_sockets_in_content_processes=true +run-sequentially = doesn't play nice with others. +[test_redirect_passing_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_redirect_different-protocol_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_reentrancy_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_resumable_channel_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_simple_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_http3_prio_disabled_wrap.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807925 +run-sequentially = http3server +[test_http3_prio_enabled_wrap.js] +skip-if = + os == 'android' + os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807925 +run-sequentially = http3server +[test_xmlhttprequest_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_XHR_redirects.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_redirect_history_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_reply_without_content_type_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_getHost_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_alt-data_simple_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_alt-data_stream_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_alt-data_closeWithStatus_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_original_sent_received_head_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_channel_id.js] +[test_trackingProtection_annotateChannels_wrap1.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_trackingProtection_annotateChannels_wrap2.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_channel_priority_wrap.js] +[test_multipart_streamconv_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_alt-data_cross_process_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_httpcancel_wrap.js] +prefs = network.allow_raw_sockets_in_content_processes=true +[test_dns_by_type_resolve_wrap.js] +[test_trr_httpssvc_wrap.js] +skip-if = os == "android" +[test_orb_empty_header_wrap.js] +[test_is_proxy_used.js] diff --git a/netwerk/test/useragent/browser_nonsnap.ini b/netwerk/test/useragent/browser_nonsnap.ini new file mode 100644 index 0000000000..b8ba79ec3e --- /dev/null +++ b/netwerk/test/useragent/browser_nonsnap.ini @@ -0,0 +1,4 @@ +[DEFAULT] + +[browser_ua_nonsnap.js] +skip-if = os != "linux" diff --git a/netwerk/test/useragent/browser_snap.ini b/netwerk/test/useragent/browser_snap.ini new file mode 100644 index 0000000000..3454f4d0f8 --- /dev/null +++ b/netwerk/test/useragent/browser_snap.ini @@ -0,0 +1,8 @@ +[DEFAULT] +environment = + SNAP_INSTANCE_NAME=firefox + SNAP_NAME=firefox + +[browser_ua_snap_ubuntu.js] +skip-if = + os != "linux" || !is_ubuntu diff --git a/netwerk/test/useragent/browser_ua_nonsnap.js b/netwerk/test/useragent/browser_ua_nonsnap.js new file mode 100644 index 0000000000..ad5093b91a --- /dev/null +++ b/netwerk/test/useragent/browser_ua_nonsnap.js @@ -0,0 +1,10 @@ +/* -*- 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"; + +add_task(function test_ua_nonsnap() { + ok(navigator.userAgent.match(/X11; Linux/)); +}); diff --git a/netwerk/test/useragent/browser_ua_snap_ubuntu.js b/netwerk/test/useragent/browser_ua_snap_ubuntu.js new file mode 100644 index 0000000000..00ed5d88b3 --- /dev/null +++ b/netwerk/test/useragent/browser_ua_snap_ubuntu.js @@ -0,0 +1,10 @@ +/* -*- 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"; + +add_task(function test_ua_snap_ubuntu() { + ok(navigator.userAgent.match(/X11; Ubuntu; Linux/)); +}); |