diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index 144ce2220483..c9dce9c9b8ec 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -11221,6 +11221,12 @@ value: false mirror: always +# Enable `Link: rel=preconnect` in 103 Early Hint response. +- name: network.early-hints.preconnect.enabled + type: RelaxedAtomicBool + value: false + mirror: always + # Whether to use the network process or not # Start a separate socket process. Performing networking on the socket process # is control by a sepparate pref diff --git a/netwerk/protocol/http/EarlyHintPreconnect.cpp b/netwerk/protocol/http/EarlyHintPreconnect.cpp new file mode 100644 index 000000000000..e4d05c073f84 --- /dev/null +++ b/netwerk/protocol/http/EarlyHintPreconnect.cpp @@ -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/. */ + +#include "EarlyHintPreconnect.h" + +#include "mozilla/CORSMode.h" +#include "mozilla/dom/Element.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsIOService.h" +#include "nsIURI.h" + +namespace mozilla::net { + +void EarlyHintPreconnect::MaybePreconnect(const LinkHeader& aHeader, + nsIURI* aBaseURI, + nsIPrincipal* aPrincipal) { + if (!StaticPrefs::network_early_hints_preconnect_enabled()) { + return; + } + + if (!gIOService) { + return; + } + + nsCOMPtr uri; + if (NS_FAILED(aHeader.NewResolveHref(getter_AddRefs(uri), aBaseURI))) { + return; + } + + // only preconnect secure context urls + if (!uri->SchemeIs("https")) { + return; + } + + // Note that the http connection manager will limit the number of connections + // we can make, so it should be fine we don't check duplicate preconnect + // attempts here. + CORSMode corsMode = dom::Element::StringToCORSMode(aHeader.mCrossOrigin); + if (corsMode == CORS_ANONYMOUS) { + gIOService->SpeculativeAnonymousConnect(uri, aPrincipal, nullptr); + } else { + gIOService->SpeculativeConnect(uri, aPrincipal, nullptr); + } +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/EarlyHintPreconnect.h b/netwerk/protocol/http/EarlyHintPreconnect.h new file mode 100644 index 000000000000..80642599c873 --- /dev/null +++ b/netwerk/protocol/http/EarlyHintPreconnect.h @@ -0,0 +1,24 @@ +/* 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 mozilla_net_EarlyHintPreconnect_h +#define mozilla_net_EarlyHintPreconnect_h + +#include "nsNetUtil.h" + +namespace mozilla::net { + +class EarlyHintPreconnect final { + public: + static void MaybePreconnect(const LinkHeader& aHeader, nsIURI* aBaseURI, + nsIPrincipal* aPrincipal); + + EarlyHintPreconnect() = delete; + EarlyHintPreconnect(const EarlyHintPreconnect&) = delete; + EarlyHintPreconnect& operator=(const EarlyHintPreconnect&) = delete; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_EarlyHintPreconnect_h diff --git a/netwerk/protocol/http/EarlyHintPreloader.cpp b/netwerk/protocol/http/EarlyHintPreloader.cpp index 961e6c03d4c6..678179be6cab 100644 --- a/netwerk/protocol/http/EarlyHintPreloader.cpp +++ b/netwerk/protocol/http/EarlyHintPreloader.cpp @@ -190,10 +190,6 @@ void EarlyHintPreloader::MaybeCreateAndInsertPreload( nsIURI* aBaseURI, nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings, const nsACString& aResponseReferrerPolicy) { - if (!aLinkHeader.mRel.LowerCaseEqualsASCII("preload")) { - return; - } - nsAttrValue as; ParseAsValue(aLinkHeader.mAs, as); diff --git a/netwerk/protocol/http/EarlyHintsService.cpp b/netwerk/protocol/http/EarlyHintsService.cpp index 3ef407a615c5..3bf84cdc33f2 100644 --- a/netwerk/protocol/http/EarlyHintsService.cpp +++ b/netwerk/protocol/http/EarlyHintsService.cpp @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "EarlyHintsService.h" +#include "EarlyHintPreconnect.h" #include "EarlyHintPreloader.h" #include "mozilla/PreloadHashKey.h" #include "mozilla/Telemetry.h" @@ -85,9 +86,13 @@ void EarlyHintsService::EarlyHint(const nsACString& aLinkHeader, for (auto& linkHeader : linkHeaders) { CollectLinkTypeTelemetry(linkHeader.mRel); - EarlyHintPreloader::MaybeCreateAndInsertPreload( - mOngoingEarlyHints, linkHeader, aBaseURI, principal, cookieJarSettings, - aReferrerPolicy); + if (linkHeader.mRel.LowerCaseEqualsLiteral("preconnect")) { + EarlyHintPreconnect::MaybePreconnect(linkHeader, aBaseURI, principal); + } else if (linkHeader.mRel.LowerCaseEqualsLiteral("preload")) { + EarlyHintPreloader::MaybeCreateAndInsertPreload( + mOngoingEarlyHints, linkHeader, aBaseURI, principal, + cookieJarSettings, aReferrerPolicy); + } } } diff --git a/netwerk/protocol/http/moz.build b/netwerk/protocol/http/moz.build index bf20a07cdec2..60e733f02396 100644 --- a/netwerk/protocol/http/moz.build +++ b/netwerk/protocol/http/moz.build @@ -98,6 +98,7 @@ UNIFIED_SOURCES += [ "ConnectionEntry.cpp", "ConnectionHandle.cpp", "DnsAndConnectSocket.cpp", + "EarlyHintPreconnect.cpp", "EarlyHintPreloader.cpp", "EarlyHintRegistrar.cpp", "EarlyHintsService.cpp", diff --git a/netwerk/protocol/http/nsHttpHandler.cpp b/netwerk/protocol/http/nsHttpHandler.cpp index 95f1a0c0892f..f5dbe59edafc 100644 --- a/netwerk/protocol/http/nsHttpHandler.cpp +++ b/netwerk/protocol/http/nsHttpHandler.cpp @@ -2262,8 +2262,12 @@ nsresult nsHttpHandler::SpeculativeConnectInternal( if (mDebugObservations && obsService) { // this is basically used for test coverage of an otherwise 'hintable' // feature + + // This is used to test if the `crossOrigin` attribute is parsed correctly. + nsPrintfCString debugURL("%s%s", aURI->GetSpecOrDefault().get(), + anonymous ? "anonymous" : "use-credentials"); obsService->NotifyObservers(nullptr, "speculative-connect-request", - nullptr); + NS_ConvertUTF8toUTF16(debugURL).get()); for (auto* cp : dom::ContentParent::AllProcesses(dom::ContentParent::eLive)) { PNeckoParent* neckoParent = diff --git a/netwerk/test/browser/browser.ini b/netwerk/test/browser/browser.ini index 64f856bacbe6..446d44de3324 100644 --- a/netwerk/test/browser/browser.ini +++ b/netwerk/test/browser/browser.ini @@ -11,6 +11,7 @@ support-files = early_hint_error.sjs early_hint_asset.sjs early_hint_asset_html.sjs + early_hint_preconnect_html.sjs post.html res.css res.css^headers^ @@ -146,3 +147,4 @@ skip-if = support-files = early_hint_referrer_policy_html.sjs early_hint_preload_test_helper.jsm +[browser_103_preconnect.js] diff --git a/netwerk/test/browser/browser_103_preconnect.js b/netwerk/test/browser/browser_103_preconnect.js new file mode 100644 index 000000000000..d1774abb7815 --- /dev/null +++ b/netwerk/test/browser/browser_103_preconnect.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.early-hints.enabled", true); +Services.prefs.setBoolPref("network.early-hints.preconnect.enabled", true); +Services.prefs.setBoolPref("network.http.debug-observations", true); + +registerCleanupFunction(function() { + Services.prefs.clearUserPref("network.early-hints.enabled"); + Services.prefs.clearUserPref("network.early-hints.preconnect.enabled"); + Services.prefs.clearUserPref("network.http.debug-observations"); +}); + +// 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() {} + ); + + if (!crossOrigin) { + crossOrigin = "anonymous"; + } + + Assert.equal(observed, `${href}/${crossOrigin}`); +} + +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/early_hint_preconnect_html.sjs b/netwerk/test/browser/early_hint_preconnect_html.sjs new file mode 100644 index 000000000000..02f832d28ca2 --- /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 = ` + + +

Test rel=preconnect

+ + `; + + 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(); +}