tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 1cd1965649cd7dfe39772e662b7c7cd3c6bf9a95
parent 83a675a5416e6e905aa8eed1e995335f88d21eb3
Author: Mike Conley <mconley@mozilla.com>
Date:   Wed, 10 Dec 2025 21:35:27 +0000

Bug 2005307 - Send pre-flight headers when communicating with MARS over OHTTP for sponsored topsites. r=home-newtab-reviewers,nbarrett

Differential Revision: https://phabricator.services.mozilla.com/D275865

Diffstat:
Mbrowser/extensions/newtab/lib/TopSitesFeed.sys.mjs | 43+++++++++++++++++++++++++++++++++++++++++++
Mbrowser/extensions/newtab/test/xpcshell/test_TopSitesFeed.js | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 138 insertions(+), 0 deletions(-)

diff --git a/browser/extensions/newtab/lib/TopSitesFeed.sys.mjs b/browser/extensions/newtab/lib/TopSitesFeed.sys.mjs @@ -162,6 +162,12 @@ const DISPLAY_FAIL_REASON_UNRESOLVED = "unresolved"; const RS_FALLBACK_BASE_URL = "https://firefox-settings-attachments.cdn.mozilla.net/"; +ChromeUtils.defineLazyGetter(lazy, "userAgent", () => { + return Cc["@mozilla.org/network/protocol;1?name=http"].getService( + Ci.nsIHttpProtocolHandler + ).userAgent; +}); + // Smart shortcuts import { RankShortcutsProvider } from "resource://newtab/lib/SmartShortcutsRanker/RankShortcuts.mjs"; @@ -644,6 +650,32 @@ export class ContileIntegration { const endpointBaseUrl = state.Prefs.values[PREF_UNIFIED_ADS_ENDPOINT]; + const preFlightConfig = + state.Prefs.values?.trainhopConfig?.marsPreFlight || {}; + + // We need some basic data that we can pass along to the ohttp request. + // We purposefully don't use ohttp on this request. We also expect to + // mostly hit the HTTP cache rather than the network with these requests. + if (preFlightConfig.enabled) { + const preflightResponse = await this._topSitesFeed.fetch( + `${endpointBaseUrl}v1/ads-preflight`, + { + method: "GET", + } + ); + const preFlight = await preflightResponse.json(); + + if (preFlight) { + // If we don't get a normalized_ua, it means it matched the default userAgent. + headers.append( + "X-User-Agent", + preFlight.normalized_ua || lazy.userAgent + ); + headers.append("X-Geoname-ID", preFlight.geoname_id); + headers.append("X-Geo-Location", preFlight.geo_location); + } + } + let blockedSponsors = this._topSitesFeed.store.getState().Prefs.values[ PREF_UNIFIED_ADS_BLOCKED_LIST @@ -693,6 +725,17 @@ export class ContileIntegration { ); return null; } + + // ObliviousHTTP.ohttpRequest only accepts a key/value object, and not + // a Headers instance. We normalize any headers to a key/value object. + // + // We use instanceof here since isInstance isn't available for + // Headers, it seems. + // eslint-disable-next-line mozilla/use-isInstance + if (options.headers && options.headers instanceof Headers) { + options.headers = Object.fromEntries(options.headers); + } + fetchPromise = lazy.ObliviousHTTP.ohttpRequest( ohttpRelayURL, config, diff --git a/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed.js b/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed.js @@ -11,6 +11,7 @@ ChromeUtils.defineESModuleGetters(this, { FilterAdult: "resource:///modules/FilterAdult.sys.mjs", NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs", PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs", PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", sinon: "resource://testing-common/Sinon.sys.mjs", @@ -3395,5 +3396,99 @@ add_task(async function test_ContileIntegration() { sandbox.restore(); } + { + info( + "TopSitesFeed._fetchSites should cast headers from a Headers object to JS object when using OHTTP" + ); + let { feed } = prepFeed(getTopSitesFeedForTest(sandbox)); + + Services.prefs.setStringPref( + "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL", + "https://relay.url" + ); + Services.prefs.setStringPref( + "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL", + "https://config.url" + ); + Services.prefs.setBoolPref( + "browser.newtabpage.activity-stream.unifiedAds.ohttp.enabled", + true + ); + feed.store.state.Prefs.values["unifiedAds.tiles.enabled"] = true; + feed.store.state.Prefs.values["unifiedAds.adsFeed.enabled"] = false; + feed.store.state.Prefs.values["unifiedAds.endpoint"] = + "https://test.endpoint/"; + feed.store.state.Prefs.values["discoverystream.placements.tiles"] = "1"; + feed.store.state.Prefs.values["discoverystream.placements.tiles.counts"] = + "1"; + feed.store.state.Prefs.values["unifiedAds.blockedAds"] = ""; + + const fakeOhttpConfig = { config: "config" }; + sandbox.stub(ObliviousHTTP, "getOHTTPConfig").resolves(fakeOhttpConfig); + + const ohttpRequestStub = sandbox + .stub(ObliviousHTTP, "ohttpRequest") + .resolves({ + ok: true, + status: 200, + headers: new Map([ + ["cache-control", "private, max-age=859, stale-if-error=10463"], + ]), + json: () => + Promise.resolve({ + 1: [ + { + block_key: 12345, + name: "test", + url: "https://www.test.com", + image_url: "images/test-com.png", + callbacks: { + click: "https://www.test-click.com", + impression: "https://www.test-impression.com", + }, + }, + ], + }), + }); + + let fetched = await feed._contile._fetchSites(); + + Assert.ok(fetched); + Assert.ok( + ohttpRequestStub.calledOnce, + "ohttpRequest should be called once" + ); + const callArgs = ohttpRequestStub.getCall(0).args; + Assert.equal(callArgs[0], "https://relay.url", "relay URL should match"); + Assert.deepEqual( + callArgs[1], + fakeOhttpConfig, + "config should be passed through" + ); + Assert.equal( + typeof callArgs[3].headers, + "object", + "headers should be a plain object" + ); + Assert.ok( + // We use instanceof here since isInstance isn't available for + // Headers, it seems. + // eslint-disable-next-line mozilla/use-isInstance + !(callArgs[3].headers instanceof Headers), + "headers should not be a Headers instance" + ); + + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL" + ); + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL" + ); + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.unifiedAds.ohttp.enabled" + ); + sandbox.restore(); + } + Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF); });