tor-browser

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

commit 37754e9621f13f16ea218edf50e3c3ab312b7c51
parent 94eaa76b21109266a669ce7e77b4c950eee5ecfd
Author: Sebastian Streich <sstreich@mozilla.com>
Date:   Wed, 29 Oct 2025 12:51:57 +0000

Bug 1990490 - Patch 2 - Extend Entitlement to contain experiment metadata r=ip-protection-reviewers,rking,jhirsch

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

Diffstat:
Mbrowser/components/ipprotection/GuardianClient.sys.mjs | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mbrowser/components/ipprotection/tests/xpcshell/test_GuardianClient.js | 165++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
2 files changed, 239 insertions(+), 40 deletions(-)

diff --git a/browser/components/ipprotection/GuardianClient.sys.mjs b/browser/components/ipprotection/GuardianClient.sys.mjs @@ -18,6 +18,14 @@ ChromeUtils.defineLazyGetter( ChromeUtils.importESModule("resource://gre/modules/HiddenFrame.sys.mjs") .HiddenBrowserManager ); +ChromeUtils.defineLazyGetter( + lazy, + "JsonSchemaValidator", + () => + ChromeUtils.importESModule( + "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs" + ).JsonSchemaValidator +); if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) { throw new Error("Guardian.sys.mjs should only run in the parent process"); @@ -209,7 +217,7 @@ export class GuardianClient { try { const entitlement = await Entitlement.fromResponse(response); if (!entitlement) { - return { status, error: "invalid_response" }; + return { status, error: "parse_error" }; } return { status, @@ -369,35 +377,43 @@ export class ProxyPass { * Immutable after creation. */ export class Entitlement { - /** True if the User has any valid subscription plan to Mozilla VPN */ + /** True if the User may Use the Autostart feature */ + autostart = false; + /** The date the entitlement was added to the user */ + created_at = new Date(); + /** True if the User has a limited bandwidth */ + limited_bandwidth = false; + /** True if the User may Use the location controls */ + location_controls = false; + /** True if the User has any valid subscription plan to the Mozilla VPN (not firefox VPN) */ subscribed = false; /** The Guardian User ID */ uid = 0; - /** The date the entitlement was added to the user */ - created_at = new Date(); - - constructor(subscribed, uid, created_at) { - if ( - typeof subscribed !== "boolean" || - typeof uid !== "number" || - typeof created_at !== "string" - ) { - throw new TypeError("Invalid arguments for Entitlement constructor"); - } - // Assert ISO 8601 format: YYYY-MM-DDTHH:mm:ss.sssZ - const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; - if (!iso8601Regex.test(created_at)) { - throw new TypeError( - "entitlementDate must be in ISO 8601 format: YYYY-MM-DDTHH:mm:ss.sssZ" - ); + /** True if the User has website inclusion */ + website_inclusion = false; + + constructor( + args = { + autostart: false, + created_at: new Date().toISOString(), + limited_bandwidth: false, + location_controls: false, + subscribed: false, + uid: 0, + website_inclusion: false, } + ) { // Ensure it parses to a valid date - const parsed = Date.parse(created_at); + const parsed = Date.parse(args.created_at); if (isNaN(parsed)) { throw new TypeError("entitlementDate is not a valid date string"); } - this.subscribed = subscribed; - this.uid = uid; + this.autostart = args.autostart; + this.limited_bandwidth = args.limited_bandwidth; + this.location_controls = args.location_controls; + this.website_inclusion = args.website_inclusion; + this.subscribed = args.subscribed; + this.uid = args.uid; this.created_at = parsed; Object.freeze(this); } @@ -407,9 +423,61 @@ export class Entitlement { return null; } return response.json().then(data => { - return new Entitlement(data.subscribed, data.uid, data.created_at); + const result = lazy.JsonSchemaValidator.validate( + data, + Entitlement.schema + ); + if (!result.valid) { + return null; + } + return new Entitlement(data); }); } + + static get schema() { + return { + $schema: "http://json-schema.org/draft-07/schema#", + title: "Entitlement", + type: "object", + properties: { + autostart: { + type: "boolean", + description: "True if the User may Use the Autostart feature", + }, + created_at: { + type: "string", + description: "The date the entitlement was added to the user", + format: "date-time", // ISO 8601 + pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$", + }, + limited_bandwidth: { + type: "boolean", + }, + location_controls: { + type: "boolean", + }, + subscribed: { + type: "boolean", + }, + uid: { + type: "integer", + }, + website_inclusion: { + type: "boolean", + }, + }, + required: [ + "autostart", + "created_at", + "limited_bandwidth", + "location_controls", + "subscribed", + "uid", + "website_inclusion", + ], + additionalProperties: true, + }; + } } /** diff --git a/browser/components/ipprotection/tests/xpcshell/test_GuardianClient.js b/browser/components/ipprotection/tests/xpcshell/test_GuardianClient.js @@ -41,6 +41,15 @@ const testGuardianConfig = server => ({ add_task(async function test_fetchUserInfo() { const ok = data => { return (request, r) => { + // Verify the Authorization header is present and correctly formatted + const authHeader = request.getHeader("Authorization"); + Assert.ok(authHeader, "Authorization header should be present"); + Assert.equal( + authHeader, + "Bearer test-token", + "Authorization header should have the correct format" + ); + r.setStatusLine(request.httpVersion, 200, "OK"); r.write(JSON.stringify(data)); }; @@ -55,6 +64,10 @@ add_task(async function test_fetchUserInfo() { subscribed: true, uid: 42, created_at: "2023-01-01T12:00:00.000Z", + limited_bandwidth: false, + location_controls: false, + autostart: false, + website_inclusion: false, }), expects: { status: 200, @@ -64,6 +77,118 @@ add_task(async function test_fetchUserInfo() { subscribed: true, uid: 42, created_at: "2023-01-01T12:00:00.000Z", + limited_bandwidth: false, + location_controls: false, + autostart: false, + website_inclusion: false, + }, + }, + }, + { + name: "Alpha experiment", + sends: ok({ + autostart: false, + created_at: "2023-09-24T12:00:00.000Z", + limited_bandwidth: false, + location_controls: false, + subscribed: true, + uid: 12345, + website_inclusion: false, + type: "alpha", + }), + expects: { + status: 200, + error: null, + validEntitlement: true, + entitlement: { + autostart: false, + limited_bandwidth: false, + location_controls: false, + subscribed: true, + uid: 12345, + website_inclusion: false, + created_at: "2023-09-24T12:00:00.000Z", + }, + }, + }, + { + name: "Beta experiment", + sends: ok({ + autostart: true, + created_at: "2023-09-24T12:30:00.000Z", + limited_bandwidth: false, + location_controls: false, + subscribed: false, + uid: 67890, + website_inclusion: true, + type: "beta", + }), + expects: { + status: 200, + error: null, + validEntitlement: true, + entitlement: { + autostart: true, + limited_bandwidth: false, + location_controls: false, + subscribed: false, + uid: 67890, + website_inclusion: true, + created_at: "2023-09-24T12:30:00.000Z", + }, + }, + }, + { + name: "gamma experiment", + sends: ok({ + autostart: true, + created_at: "2023-09-24T13:00:00.000Z", + limited_bandwidth: false, + location_controls: true, + subscribed: true, + uid: 54321, + website_inclusion: false, + type: "gamma", + }), + expects: { + status: 200, + error: null, + validEntitlement: true, + entitlement: { + autostart: true, + limited_bandwidth: false, + location_controls: true, + subscribed: true, + uid: 54321, + website_inclusion: false, + created_at: "2023-09-24T13:00:00.000Z", + }, + }, + }, + { + name: "Delta experiment", + sends: ok({ + autostart: true, + created_at: "2023-09-24T13:30:00.000Z", + limited_bandwidth: true, + location_controls: true, + subscribed: true, + uid: 13579, + website_inclusion: true, + type: "delta", + }), + expects: { + status: 200, + error: null, + validEntitlement: true, + entitlement: { + autostart: true, + limited_bandwidth: true, + location_controls: true, + subscribed: true, + uid: 13579, + website_inclusion: true, + created_at: "2023-09-24T13:30:00.000Z", }, }, }, @@ -72,7 +197,7 @@ add_task(async function test_fetchUserInfo() { sends: fail(HTTP_404), expects: { status: 404, - error: "invalid_response", + error: "parse_error", validEntitlement: false, }, }, @@ -91,6 +216,10 @@ add_task(async function test_fetchUserInfo() { subscribed: "true", // Incorrect type: should be boolean uid: "42", // Incorrect type: should be number created_at: 1234567890, // Incorrect type: should be string + limited_bandwidth: "false", // Incorrect type: should be boolean + location_controls: "true", // Incorrect type: should be boolean + autostart: "true", // Incorrect type: should be boolean + website_inclusion: "false", // Incorrect type: should be boolean }), expects: { status: 200, @@ -128,22 +257,24 @@ add_task(async function test_fetchUserInfo() { null, `${name}: entitlement should not be null` ); - Assert.equal( - entitlement.subscribed, - expects.entitlement.subscribed, - `${name}: entitlement.subscribed should match` - ); - Assert.equal( - entitlement.uid, - expects.entitlement.uid, - `${name}: entitlement.uid should match` - ); - // Compare date objects using getTime() for accurate comparison - Assert.equal( - new Date(entitlement.created_at).toISOString(), - new Date(Date.parse(expects.entitlement.created_at)).toISOString(), - `${name}: entitlement.created_at should match` - ); + for (const key of Object.keys(expects.entitlement)) { + // Special case the date case, all others can check equality directly + if (key === "created_at") { + Assert.equal( + new Date(entitlement.created_at).toISOString(), + new Date( + Date.parse(expects.entitlement.created_at) + ).toISOString(), + `${name}: entitlement.created_at should match` + ); + } else { + Assert.equal( + entitlement[key], + expects.entitlement[key], + `${name}: entitlement.${key} should match` + ); + } + } } else { Assert.equal( entitlement,