/* 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 lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs", CertUtils: "resource://gre/modules/CertUtils.sys.mjs", ServiceRequest: "resource://gre/modules/ServiceRequest.sys.mjs", }); const { NodeHTTPSServer } = ChromeUtils.importESModule( "resource://testing-common/NodeServer.sys.mjs" ); function waitForNotificationPromise(notification) { return new Promise(resolve => { function observer(aSubject, _aTopic, _aData) { info(aSubject); Services.obs.removeObserver(observer, notification); resolve(aSubject); } Services.obs.addObserver(observer, notification); }); } function openChannelPromise(url, options = { loadUsingSystemPrincipal: true }) { let uri = Services.io.newURI(url); options.uri = uri; let chan = NetUtil.newChannel(options); let flags = CL_ALLOW_UNKNOWN_CL; if (options.expectFailure) { flags |= CL_EXPECT_FAILURE; } return new Promise(resolve => { chan.asyncOpen( new ChannelListener((req, buf) => resolve({ req, buf }), null, flags) ); }); } let backupServer; const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( Ci.nsINativeDNSResolverOverride ); add_setup(async function setup() { let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( Ci.nsIX509CertDB ); addCertFromFile(certdb, "../unit/http2-ca.pem", "CTu,u,u"); Services.prefs.setBoolPref("network.essential_domains_fallback", true); backupServer = new NodeHTTPSServer(); await backupServer.start(); registerCleanupFunction(async () => { await backupServer.stop(); }); await backupServer.registerPathHandler("/stuff", (req, res) => { res.end("Good stuff"); }); // Just so we can simulate mapping the default HTTPS port and domain. Services.prefs.setStringPref( "network.socket.forcePort", `443=${backupServer.port()}` ); Services.io.addEssentialDomainMapping("aus5.mozilla.org", "foo.example.com"); let ncs = Cc[ "@mozilla.org/network/network-connectivity-service;1" ].getService(Ci.nsINetworkConnectivityService); ncs.IPv4 = Ci.nsINetworkConnectivityService.OK; }); add_task(async function test_fallback_on_dns_failure() { Services.fog.testResetFOG(); Services.obs.notifyObservers(null, "net:cancel-all-connections"); Services.dns.clearCache(true); override.addIPOverride("aus5.mozilla.org", "N/A"); override.addIPOverride("foo.example.com", "127.0.0.1"); let { buf } = await openChannelPromise("https://aus5.mozilla.org/stuff"); equal(buf, "Good stuff"); equal( await Glean.network.systemChannelUpdateStatus.dns.testGetValue(), 1, "Expecting one failed request due to DNS" ); equal( await Glean.network.retriedSystemChannelUpdateStatus.ok.testGetValue(), 1, "Expecting retried update request to succeed" ); }); add_task(async function test_dns_xhr() { Services.dns.clearCache(true); override.clearOverrides(); override.addIPOverride("aus5.mozilla.org", "N/A"); override.addIPOverride("foo.example.com", "127.0.0.1"); let text = await new Promise((resolve, reject) => { let request = new XMLHttpRequest({ mozAnon: true, mozSystem: true }); request.open("GET", `https://aus5.mozilla.org/stuff`, true); request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // Prevent the request from writing to cache. request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; request.overrideMimeType("text/plain"); request.timeout = 5000; request.addEventListener("load", () => resolve(request.responseText)); request.addEventListener("error", reject); request.addEventListener("timeout", reject); request.send(null); }); equal(text, "Good stuff"); }); add_task(async function test_dns_service_request() { Services.dns.clearCache(true); override.clearOverrides(); override.addIPOverride("aus5.mozilla.org", "N/A"); override.addIPOverride("foo.example.com", "127.0.0.1"); let text = await new Promise((resolve, reject) => { let request = new lazy.ServiceRequest({ mozAnon: true }); request.open("GET", `https://aus5.mozilla.org/stuff`, true); request.channel.notificationCallbacks = new lazy.CertUtils.BadCertHandler( !lazy.AddonSettings.UPDATE_REQUIREBUILTINCERTS ); request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // Prevent the request from writing to cache. request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; request.overrideMimeType("text/plain"); request.timeout = 5000; request.addEventListener("load", () => resolve(request.responseText)); request.addEventListener("error", reject); request.addEventListener("timeout", reject); request.send(null); }); equal(text, "Good stuff"); }); add_task(async function test_fallback_on_tls_failure() { Services.obs.notifyObservers(null, "net:cancel-all-connections"); Services.dns.clearCache(true); override.clearOverrides(); override.addIPOverride("aus5.mozilla.org", "127.0.0.1"); override.addIPOverride("foo.example.com", "127.0.0.1"); let { buf } = await openChannelPromise("https://aus5.mozilla.org/stuff"); equal(buf, "Good stuff"); }); add_task(async function test_no_fallback_with_content_principal() { Services.obs.notifyObservers(null, "net:cancel-all-connections"); Services.dns.clearCache(true); let { req } = await openChannelPromise("https://aus5.mozilla.org/stuff", { loadingPrincipal: Services.scriptSecurityManager.createContentPrincipal( Services.io.newURI("https://aus5.mozilla.org"), {} ), securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, expectFailure: true, }); // "NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_SECURITY, SSL_ERROR_BAD_CERT_DOMAIN)" equal(req.status, 0x805a2ff4); }); add_task(async function test_fallback_on_connection_failure() { Services.obs.notifyObservers(null, "net:cancel-all-connections"); Services.dns.clearCache(true); Services.prefs.setIntPref("network.http.connection-timeout", 1); override.clearOverrides(); override.addIPOverride("aus5.mozilla.org", "10.99.99.99"); // Local IP that doesn't exist (hopefully). override.addIPOverride("foo.example.com", "127.0.0.1"); await new Promise(resolve => do_timeout(100, resolve)); let chanNotif = waitForNotificationPromise("httpchannel-fallback"); let chanPromise = openChannelPromise("https://aus5.mozilla.org/stuff"); let chan = await chanNotif; equal(chan.status, Cr.NS_ERROR_NET_TIMEOUT); let { buf } = await chanPromise; equal(buf, "Good stuff"); });