tor-browser

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

debugger-fsm-context.js (8927B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /**
      8 * Finite State Machine (FSM) context for interactive debugging of tracker blocking.
      9 * This class manages the state transitions and interactions for the debugging process.
     10 */
     11 class DebuggerFSMContext {
     12  /**
     13   * Creates an instance of DebuggerFSMContext.
     14   *
     15   * @param {Array} allTrackers - A non-empty list of all trackers to be managed by the FSM context.
     16   * @param {object} callbacks - An object containing optional callback functions.
     17   * @param {Function} [callbacks.onPromptTextUpdate] - Callback invoked when the prompt text is updated.
     18   * @param {Function} [callbacks.onButtonStateUpdate] - Callback invoked when the button state is updated.
     19   * @param {Function} [callbacks.onTrackersBlockedStateUpdate] - Callback invoked when the trackers blocked state is updated.
     20   */
     21  constructor(
     22    allTrackers,
     23    { onPromptTextUpdate, onButtonStateUpdate, onTrackersBlockedStateUpdate }
     24  ) {
     25    this.allTrackers = allTrackers;
     26    this.subdomainStageTrackers = new Set();
     27    this.necessaryTrackers = new Set();
     28    this.onTrackersBlockedStateUpdate =
     29      onTrackersBlockedStateUpdate || (() => {});
     30    this.onPromptTextUpdate = onPromptTextUpdate || (() => {});
     31    this.onButtonStateUpdate = onButtonStateUpdate || (() => {});
     32    this.onTrackersBlockedStateUpdate(false, this.allTrackers);
     33    this.onButtonStateUpdate("stop-debugging", false);
     34    this.changeState(new DomainStageState(this));
     35  }
     36 
     37  /**
     38   * Transition to a new FSM state.
     39   *
     40   * @param {object} state - The new state instance.
     41   */
     42  changeState(state) {
     43    this.state = state;
     44  }
     45 
     46  /**
     47   * Called when user clicks "Continue".
     48   */
     49  async onTestNextTracker() {
     50    await this.state.onTestNextTracker();
     51  }
     52 
     53  /**
     54   * Called when user clicks "Website Broke".
     55   */
     56  async onWebsiteBroke() {
     57    await this.state.onWebsiteBroke();
     58  }
     59 
     60  /**
     61   * Stop interactive debugging and reset all states.
     62   */
     63  async stop() {
     64    this.onPromptTextUpdate(undefined, `Interactive debugger stopped.`);
     65    await this.onTrackersBlockedStateUpdate(false, this.allTrackers);
     66    this.onButtonStateUpdate("test-next-tracker", true);
     67    this.onButtonStateUpdate("website-broke", true);
     68    this.onButtonStateUpdate("stop-debugging", true);
     69  }
     70 }
     71 
     72 class FSMState {
     73  /**
     74   * Base class for FSM states.
     75   *
     76   * @param {DebuggerFSMContext} debuggerFSMContext - The FSM context.
     77   */
     78  constructor(debuggerFSMContext) {
     79    this.debuggerFSMContext = debuggerFSMContext;
     80  }
     81 
     82  /**
     83   * Handle the "Continue" button click to test the next tracker.
     84   */
     85  onTestNextTracker() {
     86    throw new Error("onTestNextTracker must be implemented in derived class");
     87  }
     88 
     89  /**
     90   * Handle the "Website Broke" button click to unblock the last tracker.
     91   */
     92  onWebsiteBroke() {
     93    throw new Error("onWebsiteBroke must be implemented in derived class");
     94  }
     95 }
     96 
     97 /**
     98 * This stage groups trackers by their top-level domain and allows the user to test
     99 * each domain group separately. If a website breaks, the user can use `onWebsiteBroke`
    100 * to unblock the last group and continue testing the next domain group.
    101 * If the website is not broken, the user can use `onTestNextTracker` to test the next domain group.
    102 * If all groups are tested, it transitions to the individual domain stage.
    103 */
    104 class DomainStageState extends FSMState {
    105  constructor(debuggerFSMContext) {
    106    super(debuggerFSMContext);
    107    this.domainGroups = this.groupByDomain(debuggerFSMContext.allTrackers);
    108    this.lastGroup = null;
    109    this.debuggerFSMContext.onPromptTextUpdate(
    110      this.domainGroups.length,
    111      "Click 'Continue' to start domain debugging."
    112    );
    113    this.debuggerFSMContext.onButtonStateUpdate("test-next-tracker", false);
    114  }
    115 
    116  /**
    117   * Handle the "Continue" button click to test the next domain group.
    118   */
    119  async onTestNextTracker() {
    120    this.lastGroup = this.domainGroups.shift();
    121    const count = this.domainGroups.length;
    122    // If no more domain groups to check, transition to subdomain stage
    123    if (!this.lastGroup) {
    124      this.debuggerFSMContext.onPromptTextUpdate(
    125        count,
    126        "Domain debugging finished. Starting subdomain tracker stage. Click 'Continue' to proceed."
    127      );
    128      this.debuggerFSMContext.onButtonStateUpdate("website-broke", true);
    129      this.debuggerFSMContext.changeState(
    130        new SubdomainStageState(this.debuggerFSMContext)
    131      );
    132      return;
    133    }
    134    await this.debuggerFSMContext.onTrackersBlockedStateUpdate(
    135      true,
    136      this.lastGroup.hosts
    137    );
    138    this.debuggerFSMContext.onButtonStateUpdate("website-broke", false);
    139    this.debuggerFSMContext.onPromptTextUpdate(
    140      count,
    141      `Blocked domain group '${this.lastGroup.domain}'. If the website is broken, click 'Website Broke', otherwise 'Continue'.`
    142    );
    143  }
    144 
    145  /**
    146   * Handle the "Website Broke" button click to unblock the last group.
    147   */
    148  async onWebsiteBroke() {
    149    if (this.lastGroup && this.lastGroup.hosts) {
    150      this.lastGroup.hosts.forEach(tracker =>
    151        this.debuggerFSMContext.subdomainStageTrackers.add(tracker)
    152      );
    153      // Unblock the group to restore site
    154      await this.debuggerFSMContext.onTrackersBlockedStateUpdate(
    155        false,
    156        this.lastGroup.hosts
    157      );
    158      const count = this.domainGroups.length;
    159      this.debuggerFSMContext.onPromptTextUpdate(
    160        count,
    161        `Domain group '${this.lastGroup.domain}' will be tested individually later. Click 'Continue' to test the next domain group.`
    162      );
    163      this.debuggerFSMContext.onButtonStateUpdate("website-broke", true);
    164    }
    165  }
    166 
    167  /**
    168   * Group trackers by their top-level domain.
    169   *
    170   * @param {string[]} trackers - List of tracker hostnames.
    171   */
    172  groupByDomain(trackers) {
    173    const domainGroupsMap = {};
    174    trackers.forEach(tracker => {
    175      const domain = Services.eTLD.getBaseDomainFromHost(tracker);
    176      if (!domainGroupsMap[domain]) {
    177        domainGroupsMap[domain] = new Set();
    178      }
    179      domainGroupsMap[domain].add(tracker);
    180    });
    181    return Object.entries(domainGroupsMap).map(([domain, hosts]) => ({
    182      domain,
    183      hosts: Array.from(hosts),
    184    }));
    185  }
    186 }
    187 
    188 /**
    189 * Subdomain stage: block/unblock each tracker separately. This stage is very similar to the domain stage,
    190 * but it allows the user to test each subdomain tracker individually. This ensures we can identify
    191 * which specific subdomain tracker is causing issues, rather than just the top-level domain.
    192 */
    193 class SubdomainStageState extends FSMState {
    194  constructor(debuggerFSMContext) {
    195    super(debuggerFSMContext);
    196    this.subdomains = Array.from(debuggerFSMContext.subdomainStageTrackers);
    197    this.lastSubdomain = null;
    198  }
    199 
    200  async onTestNextTracker() {
    201    this.lastSubdomain = this.subdomains.shift();
    202    const count = this.subdomains.length;
    203    if (!this.lastSubdomain) {
    204      this.debuggerFSMContext.changeState(
    205        new CompletedState(this.debuggerFSMContext)
    206      );
    207      return;
    208    }
    209    await this.debuggerFSMContext.onTrackersBlockedStateUpdate(true, [
    210      this.lastSubdomain,
    211    ]);
    212    this.debuggerFSMContext.onButtonStateUpdate("website-broke", false);
    213    this.debuggerFSMContext.onPromptTextUpdate(
    214      count,
    215      `Blocked subdomain '${this.lastSubdomain}'. If the website is broken, click 'Website Broke', otherwise 'Continue'.`
    216    );
    217  }
    218 
    219  async onWebsiteBroke() {
    220    if (this.lastSubdomain) {
    221      this.debuggerFSMContext.necessaryTrackers.add(this.lastSubdomain);
    222      await this.debuggerFSMContext.onTrackersBlockedStateUpdate(false, [
    223        this.lastSubdomain,
    224      ]);
    225      const count = this.subdomains.length;
    226      this.debuggerFSMContext.onButtonStateUpdate("website-broke", true);
    227      this.debuggerFSMContext.onPromptTextUpdate(
    228        count,
    229        `Added '${this.lastSubdomain}' to necessary trackers. Click 'Continue' to test the next subdomain.`
    230      );
    231    }
    232  }
    233 }
    234 
    235 class CompletedState extends FSMState {
    236  constructor(debuggerFSMContext) {
    237    super(debuggerFSMContext);
    238    this.debuggerFSMContext.onPromptTextUpdate(
    239      undefined,
    240      `Subdomain debugging finished. Please add the following to the exceptions list: ${Array.from(this.debuggerFSMContext.necessaryTrackers).join(", ")}`,
    241      true
    242    );
    243    this.debuggerFSMContext.onButtonStateUpdate("test-next-tracker", true);
    244    this.debuggerFSMContext.onButtonStateUpdate("website-broke", true);
    245    this.debuggerFSMContext.onButtonStateUpdate("stop-debugging", true);
    246  }
    247 }
    248 
    249 // Only export for Node.js (test) environments
    250 if (typeof module !== "undefined" && module.exports) {
    251  module.exports = {
    252    DebuggerFSMContext,
    253    DomainStageState,
    254    SubdomainStageState,
    255    CompletedState,
    256  };
    257 }
    258 
    259 exports.DebuggerFSMContext = DebuggerFSMContext;