summaryrefslogtreecommitdiffstats
path: root/browser/actors/AboutProtectionsParent.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/actors/AboutProtectionsParent.sys.mjs')
-rw-r--r--browser/actors/AboutProtectionsParent.sys.mjs444
1 files changed, 444 insertions, 0 deletions
diff --git a/browser/actors/AboutProtectionsParent.sys.mjs b/browser/actors/AboutProtectionsParent.sys.mjs
new file mode 100644
index 0000000000..c1586559a6
--- /dev/null
+++ b/browser/actors/AboutProtectionsParent.sys.mjs
@@ -0,0 +1,444 @@
+/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
+ LoginBreaches: "resource:///modules/LoginBreaches.sys.mjs",
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+ Region: "resource://gre/modules/Region.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.js",
+ FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.js",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => {
+ return ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+ ).getFxAccountsSingleton();
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "TrackingDBService",
+ "@mozilla.org/tracking-db-service;1",
+ "nsITrackingDBService"
+);
+
+let idToTextMap = new Map([
+ [Ci.nsITrackingDBService.TRACKERS_ID, "tracker"],
+ [Ci.nsITrackingDBService.TRACKING_COOKIES_ID, "cookie"],
+ [Ci.nsITrackingDBService.CRYPTOMINERS_ID, "cryptominer"],
+ [Ci.nsITrackingDBService.FINGERPRINTERS_ID, "fingerprinter"],
+ [Ci.nsITrackingDBService.SOCIAL_ID, "social"],
+]);
+
+const MONITOR_API_ENDPOINT = Services.urlFormatter.formatURLPref(
+ "browser.contentblocking.report.endpoint_url"
+);
+
+const SECURE_PROXY_ADDON_ID = "secure-proxy@mozilla.com";
+
+const SCOPE_MONITOR = [
+ "profile:uid",
+ "https://identity.mozilla.com/apps/monitor",
+];
+
+const SCOPE_VPN = "profile https://identity.mozilla.com/account/subscriptions";
+const VPN_ENDPOINT = `${Services.prefs.getStringPref(
+ "identity.fxaccounts.auth.uri"
+)}oauth/subscriptions/active`;
+
+// The ID of the vpn subscription, if we see this ID attached to a user's account then they have subscribed to vpn.
+const VPN_SUB_ID = Services.prefs.getStringPref(
+ "browser.contentblocking.report.vpn_sub_id"
+);
+
+// Error messages
+const INVALID_OAUTH_TOKEN = "Invalid OAuth token";
+const USER_UNSUBSCRIBED_TO_MONITOR = "User is not subscribed to Monitor";
+const SERVICE_UNAVAILABLE = "Service unavailable";
+const UNEXPECTED_RESPONSE = "Unexpected response";
+const UNKNOWN_ERROR = "Unknown error";
+
+// Valid response info for successful Monitor data
+const MONITOR_RESPONSE_PROPS = [
+ "monitoredEmails",
+ "numBreaches",
+ "passwords",
+ "numBreachesResolved",
+ "passwordsResolved",
+];
+
+let gTestOverride = null;
+let monitorResponse = null;
+let entrypoint = "direct";
+
+export class AboutProtectionsParent extends JSWindowActorParent {
+ constructor() {
+ super();
+ }
+
+ // Some tests wish to override certain functions with ones that mostly do nothing.
+ static setTestOverride(callback) {
+ gTestOverride = callback;
+ }
+
+ /**
+ * Fetches and validates data from the Monitor endpoint. If successful, then return
+ * expected data. Otherwise, throw the appropriate error depending on the status code.
+ *
+ * @return valid data from endpoint.
+ */
+ async fetchUserBreachStats(token) {
+ if (monitorResponse && monitorResponse.timestamp) {
+ var timeDiff = Date.now() - monitorResponse.timestamp;
+ let oneDayInMS = 24 * 60 * 60 * 1000;
+ if (timeDiff >= oneDayInMS) {
+ monitorResponse = null;
+ } else {
+ return monitorResponse;
+ }
+ }
+
+ // Make the request
+ const headers = new Headers();
+ headers.append("Authorization", `Bearer ${token}`);
+ const request = new Request(MONITOR_API_ENDPOINT, { headers });
+ const response = await fetch(request);
+
+ if (response.ok) {
+ // Validate the shape of the response is what we're expecting.
+ const json = await response.json();
+
+ // Make sure that we're getting the expected data.
+ let isValid = null;
+ for (let prop in json) {
+ isValid = MONITOR_RESPONSE_PROPS.includes(prop);
+
+ if (!isValid) {
+ break;
+ }
+ }
+
+ monitorResponse = isValid ? json : new Error(UNEXPECTED_RESPONSE);
+ if (isValid) {
+ monitorResponse.timestamp = Date.now();
+ }
+ } else {
+ // Check the reason for the error
+ switch (response.status) {
+ case 400:
+ case 401:
+ monitorResponse = new Error(INVALID_OAUTH_TOKEN);
+ break;
+ case 404:
+ monitorResponse = new Error(USER_UNSUBSCRIBED_TO_MONITOR);
+ break;
+ case 503:
+ monitorResponse = new Error(SERVICE_UNAVAILABLE);
+ break;
+ default:
+ monitorResponse = new Error(UNKNOWN_ERROR);
+ break;
+ }
+ }
+
+ if (monitorResponse instanceof Error) {
+ throw monitorResponse;
+ }
+ return monitorResponse;
+ }
+
+ /**
+ * Retrieves login data for the user.
+ *
+ * @return {{
+ * numLogins: Number,
+ * potentiallyBreachedLogins: Number,
+ * mobileDeviceConnected: Boolean }}
+ */
+ async getLoginData() {
+ if (gTestOverride && "getLoginData" in gTestOverride) {
+ return gTestOverride.getLoginData();
+ }
+
+ try {
+ if (await lazy.fxAccounts.getSignedInUser()) {
+ await lazy.fxAccounts.device.refreshDeviceList();
+ }
+ } catch (e) {
+ console.error("There was an error fetching login data: ", e.message);
+ }
+
+ const userFacingLogins =
+ Services.logins.countLogins("", "", "") -
+ Services.logins.countLogins(
+ lazy.FXA_PWDMGR_HOST,
+ null,
+ lazy.FXA_PWDMGR_REALM
+ );
+
+ let potentiallyBreachedLogins = null;
+ // Get the stats for number of potentially breached Lockwise passwords
+ // if the Primary Password isn't locked.
+ if (userFacingLogins && Services.logins.isLoggedIn) {
+ const logins = await lazy.LoginHelper.getAllUserFacingLogins();
+ potentiallyBreachedLogins =
+ await lazy.LoginBreaches.getPotentialBreachesByLoginGUID(logins);
+ }
+
+ let mobileDeviceConnected =
+ lazy.fxAccounts.device.recentDeviceList &&
+ lazy.fxAccounts.device.recentDeviceList.filter(
+ device => device.type == "mobile"
+ ).length;
+
+ return {
+ numLogins: userFacingLogins,
+ potentiallyBreachedLogins: potentiallyBreachedLogins
+ ? potentiallyBreachedLogins.size
+ : 0,
+ mobileDeviceConnected,
+ };
+ }
+
+ /**
+ * Retrieves monitor data for the user.
+ *
+ * @return {{ monitoredEmails: Number,
+ * numBreaches: Number,
+ * passwords: Number,
+ * userEmail: String|null,
+ * error: Boolean }}
+ * Monitor data.
+ */
+ async getMonitorData() {
+ if (gTestOverride && "getMonitorData" in gTestOverride) {
+ monitorResponse = gTestOverride.getMonitorData();
+ monitorResponse.timestamp = Date.now();
+ // In a test, expect this to not fetch from the monitor endpoint due to the timestamp guaranteeing we use the cache.
+ monitorResponse = await this.fetchUserBreachStats();
+ return monitorResponse;
+ }
+
+ let monitorData = {};
+ let userEmail = null;
+ let token = await this.getMonitorScopedOAuthToken();
+
+ try {
+ if (token) {
+ monitorData = await this.fetchUserBreachStats(token);
+
+ // Send back user's email so the protections report can direct them to the proper
+ // OAuth flow on Monitor.
+ const { email } = await lazy.fxAccounts.getSignedInUser();
+ userEmail = email;
+ } else {
+ // If no account exists, then the user is not logged in with an fxAccount.
+ monitorData = {
+ errorMessage: "No account",
+ };
+ }
+ } catch (e) {
+ console.error(e.message);
+ monitorData.errorMessage = e.message;
+
+ // If the user's OAuth token is invalid, we clear the cached token and refetch
+ // again. If OAuth token is invalid after the second fetch, then the monitor UI
+ // will simply show the "no logins" UI version.
+ if (e.message === INVALID_OAUTH_TOKEN) {
+ await lazy.fxAccounts.removeCachedOAuthToken({ token });
+ token = await this.getMonitorScopedOAuthToken();
+
+ try {
+ monitorData = await this.fetchUserBreachStats(token);
+ } catch (_) {
+ console.error(e.message);
+ }
+ } else if (e.message === USER_UNSUBSCRIBED_TO_MONITOR) {
+ // Send back user's email so the protections report can direct them to the proper
+ // OAuth flow on Monitor.
+ const { email } = await lazy.fxAccounts.getSignedInUser();
+ userEmail = email;
+ } else {
+ monitorData.errorMessage = e.message || "An error ocurred.";
+ }
+ }
+
+ return {
+ ...monitorData,
+ userEmail,
+ error: !!monitorData.errorMessage,
+ };
+ }
+
+ async getMonitorScopedOAuthToken() {
+ let token = null;
+
+ try {
+ token = await lazy.fxAccounts.getOAuthToken({ scope: SCOPE_MONITOR });
+ } catch (e) {
+ console.error(
+ "There was an error fetching the user's token: ",
+ e.message
+ );
+ }
+
+ return token;
+ }
+
+ /**
+ * The proxy card will only show if the user is in the US, has the browser language in "en-US",
+ * and does not yet have Proxy installed.
+ */
+ async shouldShowProxyCard() {
+ const region = lazy.Region.home || "";
+ const languages = Services.prefs.getComplexValue(
+ "intl.accept_languages",
+ Ci.nsIPrefLocalizedString
+ );
+ const alreadyInstalled = await lazy.AddonManager.getAddonByID(
+ SECURE_PROXY_ADDON_ID
+ );
+
+ return (
+ region.toLowerCase() === "us" &&
+ !alreadyInstalled &&
+ languages.data.toLowerCase().includes("en-us")
+ );
+ }
+
+ async VPNSubStatus() {
+ // For testing, set vpn sub status manually
+ if (gTestOverride && "vpnOverrides" in gTestOverride) {
+ return gTestOverride.vpnOverrides();
+ }
+
+ let vpnToken;
+ try {
+ vpnToken = await lazy.fxAccounts.getOAuthToken({ scope: SCOPE_VPN });
+ } catch (e) {
+ console.error(
+ "There was an error fetching the user's token: ",
+ e.message
+ );
+ // there was an error, assume user is not subscribed to VPN
+ return false;
+ }
+ let headers = new Headers();
+ headers.append("Authorization", `Bearer ${vpnToken}`);
+ const request = new Request(VPN_ENDPOINT, { headers });
+ const res = await fetch(request);
+ if (res.ok) {
+ const result = await res.json();
+ for (let sub of result) {
+ if (sub.subscriptionId == VPN_SUB_ID) {
+ return true;
+ }
+ }
+ return false;
+ }
+ // unknown logic: assume user is not subscribed to VPN
+ return false;
+ }
+
+ async receiveMessage(aMessage) {
+ let win = this.browsingContext.top.embedderElement.ownerGlobal;
+ switch (aMessage.name) {
+ case "OpenAboutLogins":
+ lazy.LoginHelper.openPasswordManager(win, {
+ entryPoint: "aboutprotections",
+ });
+ break;
+ case "OpenContentBlockingPreferences":
+ win.openPreferences("privacy-trackingprotection", {
+ origin: "about-protections",
+ });
+ break;
+ case "OpenSyncPreferences":
+ win.openTrustedLinkIn("about:preferences#sync", "tab");
+ break;
+ case "FetchContentBlockingEvents":
+ let dataToSend = {};
+ let displayNames = new Services.intl.DisplayNames(undefined, {
+ type: "weekday",
+ style: "abbreviated",
+ calendar: "gregory",
+ });
+
+ // Weekdays starting Sunday (7) to Saturday (6).
+ let weekdays = [7, 1, 2, 3, 4, 5, 6].map(day => displayNames.of(day));
+ dataToSend.weekdays = weekdays;
+
+ if (lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
+ dataToSend.isPrivate = true;
+ return dataToSend;
+ }
+ let sumEvents = await lazy.TrackingDBService.sumAllEvents();
+ let earliestDate =
+ await lazy.TrackingDBService.getEarliestRecordedDate();
+ let eventsByDate = await lazy.TrackingDBService.getEventsByDateRange(
+ aMessage.data.from,
+ aMessage.data.to
+ );
+ let largest = 0;
+
+ for (let result of eventsByDate) {
+ let count = result.getResultByName("count");
+ let type = result.getResultByName("type");
+ let timestamp = result.getResultByName("timestamp");
+ dataToSend[timestamp] = dataToSend[timestamp] || { total: 0 };
+ dataToSend[timestamp][idToTextMap.get(type)] = count;
+ dataToSend[timestamp].total += count;
+ // Record the largest amount of tracking events found per day,
+ // to create the tallest column on the graph and compare other days to.
+ if (largest < dataToSend[timestamp].total) {
+ largest = dataToSend[timestamp].total;
+ }
+ }
+ dataToSend.largest = largest;
+ dataToSend.earliestDate = earliestDate;
+ dataToSend.sumEvents = sumEvents;
+
+ return dataToSend;
+
+ case "FetchMonitorData":
+ return this.getMonitorData();
+
+ case "FetchUserLoginsData":
+ return this.getLoginData();
+
+ case "ClearMonitorCache":
+ monitorResponse = null;
+ break;
+
+ case "GetShowProxyCard":
+ let card = await this.shouldShowProxyCard();
+ return card;
+
+ case "RecordEntryPoint":
+ entrypoint = aMessage.data.entrypoint;
+ break;
+
+ case "FetchEntryPoint":
+ return entrypoint;
+
+ case "FetchVPNSubStatus":
+ return this.VPNSubStatus();
+
+ case "FetchShowVPNCard":
+ return lazy.BrowserUtils.shouldShowVPNPromo();
+ }
+
+ return undefined;
+ }
+}