tor-browser

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

GeckoViewProgress.sys.mjs (17978B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
      6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 
      8 const lazy = {};
      9 
     10 XPCOMUtils.defineLazyServiceGetter(
     11  lazy,
     12  "OverrideService",
     13  "@mozilla.org/security/certoverride;1",
     14  Ci.nsICertOverrideService
     15 );
     16 
     17 XPCOMUtils.defineLazyServiceGetter(
     18  lazy,
     19  "IDNService",
     20  "@mozilla.org/network/idn-service;1",
     21  Ci.nsIIDNService
     22 );
     23 
     24 ChromeUtils.defineESModuleGetters(lazy, {
     25  BrowserTelemetryUtils: "resource://gre/modules/BrowserTelemetryUtils.sys.mjs",
     26  GleanStopwatch: "resource://gre/modules/GeckoViewTelemetry.sys.mjs",
     27 });
     28 
     29 var IdentityHandler = {
     30  // The definitions below should be kept in sync with those in GeckoView.ProgressListener.SecurityInformation
     31  // No trusted identity information. No site identity icon is shown.
     32  IDENTITY_MODE_UNKNOWN: 0,
     33 
     34  // Domain-Validation SSL CA-signed domain verification (DV).
     35  IDENTITY_MODE_IDENTIFIED: 1,
     36 
     37  // Extended-Validation SSL CA-signed identity information (EV). A more rigorous validation process.
     38  IDENTITY_MODE_VERIFIED: 2,
     39 
     40  // The following mixed content modes are only used if "security.mixed_content.block_active_content"
     41  // is enabled. Our Java frontend coalesces them into one indicator.
     42 
     43  // No mixed content information. No mixed content icon is shown.
     44  MIXED_MODE_UNKNOWN: 0,
     45 
     46  // Blocked active mixed content.
     47  MIXED_MODE_CONTENT_BLOCKED: 1,
     48 
     49  // Loaded active mixed content.
     50  MIXED_MODE_CONTENT_LOADED: 2,
     51 
     52  /**
     53   * Determines the identity mode corresponding to the icon we show in the urlbar.
     54   */
     55  getIdentityMode: function getIdentityMode(aState) {
     56    if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
     57      return this.IDENTITY_MODE_VERIFIED;
     58    }
     59 
     60    if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE) {
     61      return this.IDENTITY_MODE_IDENTIFIED;
     62    }
     63 
     64    return this.IDENTITY_MODE_UNKNOWN;
     65  },
     66 
     67  getMixedDisplayMode: function getMixedDisplayMode(aState) {
     68    if (aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT) {
     69      return this.MIXED_MODE_CONTENT_LOADED;
     70    }
     71 
     72    if (
     73      aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT
     74    ) {
     75      return this.MIXED_MODE_CONTENT_BLOCKED;
     76    }
     77 
     78    return this.MIXED_MODE_UNKNOWN;
     79  },
     80 
     81  getMixedActiveMode: function getActiveDisplayMode(aState) {
     82    // Only show an indicator for loaded mixed content if the pref to block it is enabled
     83    if (
     84      aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT &&
     85      !Services.prefs.getBoolPref("security.mixed_content.block_active_content")
     86    ) {
     87      return this.MIXED_MODE_CONTENT_LOADED;
     88    }
     89 
     90    if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
     91      return this.MIXED_MODE_CONTENT_BLOCKED;
     92    }
     93 
     94    return this.MIXED_MODE_UNKNOWN;
     95  },
     96 
     97  /**
     98   * Determine the identity of the page being displayed by examining its SSL cert
     99   * (if available). Return the data needed to update the UI.
    100   */
    101  checkIdentity: function checkIdentity(aState, aBrowser) {
    102    const identityMode = this.getIdentityMode(aState);
    103    const mixedDisplay = this.getMixedDisplayMode(aState);
    104    const mixedActive = this.getMixedActiveMode(aState);
    105    const result = {
    106      mode: {
    107        identity: identityMode,
    108        mixed_display: mixedDisplay,
    109        mixed_active: mixedActive,
    110      },
    111    };
    112 
    113    if (aBrowser.contentPrincipal) {
    114      result.origin = aBrowser.contentPrincipal.originNoSuffix;
    115    }
    116 
    117    // Don't show identity data for pages with an unknown identity or if any
    118    // mixed content is loaded (mixed display content is loaded by default).
    119    if (
    120      identityMode === this.IDENTITY_MODE_UNKNOWN ||
    121      aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN ||
    122      aState & Ci.nsIWebProgressListener.STATE_IS_INSECURE
    123    ) {
    124      result.secure = false;
    125      return result;
    126    }
    127 
    128    result.secure = true;
    129 
    130    let uri = aBrowser.currentURI || {};
    131    try {
    132      uri = Services.io.createExposableURI(uri);
    133    } catch (e) {}
    134 
    135    try {
    136      result.host = lazy.IDNService.convertToDisplayIDN(uri.host);
    137    } catch (e) {
    138      result.host = uri.host;
    139    }
    140 
    141    if (!aBrowser.securityUI.secInfo) {
    142      return result;
    143    }
    144 
    145    const cert = aBrowser.securityUI.secInfo.serverCert;
    146 
    147    result.certificate =
    148      aBrowser.securityUI.secInfo.serverCert.getBase64DERString();
    149 
    150    try {
    151      result.securityException = lazy.OverrideService.hasMatchingOverride(
    152        uri.host,
    153        uri.port,
    154        {},
    155        cert,
    156        {}
    157      );
    158 
    159      // If an override exists, the connection is being allowed but should not
    160      // be considered secure.
    161      result.secure = !result.securityException;
    162    } catch (e) {}
    163 
    164    return result;
    165  },
    166 };
    167 
    168 class Tracker {
    169  constructor(aModule) {
    170    this._module = aModule;
    171  }
    172 
    173  get eventDispatcher() {
    174    return this._module.eventDispatcher;
    175  }
    176 
    177  get browser() {
    178    return this._module.browser;
    179  }
    180 
    181  QueryInterface = ChromeUtils.generateQI(["nsIWebProgressListener"]);
    182 }
    183 
    184 class ProgressTracker extends Tracker {
    185  constructor(aModule) {
    186    super(aModule);
    187 
    188    this.pageLoadStopwatch = new lazy.GleanStopwatch(
    189      Glean.geckoview.pageLoadTime
    190    );
    191    this.pageReloadStopwatch = new lazy.GleanStopwatch(
    192      Glean.geckoview.pageReloadTime
    193    );
    194    this.pageLoadProgressStopwatch = new lazy.GleanStopwatch(
    195      Glean.geckoview.pageLoadProgressTime
    196    );
    197 
    198    this.clear();
    199    this._eventReceived = null;
    200  }
    201 
    202  start(aUri) {
    203    debug`ProgressTracker start ${aUri}`;
    204 
    205    if (this._eventReceived) {
    206      // A request was already in process, let's cancel it
    207      this.stop(/* isSuccess */ false);
    208    }
    209 
    210    this._eventReceived = new Set();
    211    this.clear();
    212    const data = this._data;
    213 
    214    if (aUri === "about:blank") {
    215      data.uri = null;
    216      return;
    217    }
    218 
    219    this.pageLoadProgressStopwatch.start();
    220 
    221    data.uri = aUri;
    222    data.pageStart = true;
    223    this.updateProgress();
    224  }
    225 
    226  changeLocation(aUri) {
    227    debug`ProgressTracker changeLocation ${aUri}`;
    228 
    229    const data = this._data;
    230    data.locationChange = true;
    231    data.uri = aUri;
    232  }
    233 
    234  stop(aIsSuccess) {
    235    debug`ProgressTracker stop`;
    236 
    237    if (!this._eventReceived) {
    238      // No request in progress
    239      return;
    240    }
    241 
    242    if (aIsSuccess) {
    243      this.pageLoadProgressStopwatch.finish();
    244    } else {
    245      this.pageLoadProgressStopwatch.cancel();
    246    }
    247 
    248    const data = this._data;
    249    data.pageStop = true;
    250    this.updateProgress();
    251    this._eventReceived = null;
    252  }
    253 
    254  onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
    255    debug`ProgressTracker onStateChange: isTopLevel=${aWebProgress.isTopLevel},
    256                          flags=${aStateFlags}, status=${aStatus}`;
    257 
    258    if (!aWebProgress || !aWebProgress.isTopLevel) {
    259      return;
    260    }
    261 
    262    const { displaySpec } = aRequest.QueryInterface(Ci.nsIChannel).URI;
    263 
    264    if (aRequest.URI.schemeIs("about")) {
    265      return;
    266    }
    267 
    268    debug`ProgressTracker onStateChange: uri=${displaySpec}`;
    269 
    270    const isPageReload =
    271      (aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD) != 0;
    272    const stopwatch = isPageReload
    273      ? this.pageReloadStopwatch
    274      : this.pageLoadStopwatch;
    275 
    276    const isStart = (aStateFlags & Ci.nsIWebProgressListener.STATE_START) != 0;
    277    const isStop = (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) != 0;
    278    const isRedirecting =
    279      (aStateFlags & Ci.nsIWebProgressListener.STATE_REDIRECTING) != 0;
    280 
    281    if (isStart) {
    282      stopwatch.start();
    283      this.start(displaySpec);
    284    } else if (isStop && !aWebProgress.isLoadingDocument) {
    285      stopwatch.finish();
    286      this.stop(aStatus == Cr.NS_OK);
    287    } else if (isRedirecting) {
    288      stopwatch.start();
    289      this.start(displaySpec);
    290    }
    291 
    292    // During history naviation, global window is recycled, so pagetitlechanged isn't fired
    293    // Although Firefox Desktop always set title by onLocationChange, to reduce title change call,
    294    // we only send title during history navigation.
    295    if ((aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) != 0) {
    296      this.eventDispatcher.sendRequest({
    297        type: "GeckoView:PageTitleChanged",
    298        title: this.browser.contentTitle,
    299      });
    300    }
    301  }
    302 
    303  onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
    304    if (
    305      !aWebProgress ||
    306      !aWebProgress.isTopLevel ||
    307      !aLocationURI ||
    308      aLocationURI.schemeIs("about")
    309    ) {
    310      return;
    311    }
    312 
    313    debug`ProgressTracker onLocationChange: location=${aLocationURI.displaySpec},
    314                             flags=${aFlags}`;
    315 
    316    if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
    317      this.stop(/* isSuccess */ false);
    318    } else {
    319      this.changeLocation(aLocationURI.displaySpec);
    320    }
    321  }
    322 
    323  handleEvent(aEvent) {
    324    if (!this._eventReceived || this._eventReceived.has(aEvent.name)) {
    325      // Either we're not tracking or we have received this event already
    326      return;
    327    }
    328 
    329    const data = this._data;
    330 
    331    if (!data.uri || data.uri !== aEvent.data?.uri) {
    332      return;
    333    }
    334 
    335    debug`ProgressTracker handleEvent: ${aEvent.name}`;
    336 
    337    let needsUpdate = false;
    338 
    339    switch (aEvent.name) {
    340      case "DOMContentLoaded":
    341        needsUpdate = needsUpdate || !data.parsed;
    342        // if page_load.progressbar_completion is set to 1, we complete the page load progress when
    343        // DOMContentLoaded is received.
    344        if (this._progressBarCompletion == 1) {
    345          data.completeProgress = true;
    346        } else {
    347          data.parsed = true;
    348        }
    349        break;
    350      case "MozAfterPaint":
    351        needsUpdate = needsUpdate || !data.firstPaint;
    352        // if page_load.progressbar_completion is set to 2, we complete the page load progress at
    353        // the first MozAfterPaint after DOMContentLoaded is received.
    354        if (this._progressBarCompletion == 2 && data.parsed) {
    355          data.completeProgress = true;
    356        } else {
    357          data.firstPaint = true;
    358        }
    359        break;
    360      case "pageshow":
    361        needsUpdate = needsUpdate || !data.pageShow;
    362        data.pageShow = true;
    363        break;
    364    }
    365 
    366    this._eventReceived.add(aEvent.name);
    367 
    368    if (needsUpdate) {
    369      this.updateProgress();
    370    }
    371  }
    372 
    373  clear() {
    374    this._data = {
    375      prev: 0,
    376      uri: null,
    377      locationChange: false,
    378      pageStart: false,
    379      pageStop: false,
    380      firstPaint: false,
    381      pageShow: false,
    382      parsed: false,
    383      completeProgress: false,
    384    };
    385    this._progressBarCompletion = Services.prefs.getIntPref(
    386      "page_load.progressbar_completion"
    387    );
    388  }
    389 
    390  _debugData() {
    391    return {
    392      prev: this._data.prev,
    393      uri: this._data.uri,
    394      locationChange: this._data.locationChange,
    395      pageStart: this._data.pageStart,
    396      pageStop: this._data.pageStop,
    397      firstPaint: this._data.firstPaint,
    398      pageShow: this._data.pageShow,
    399      parsed: this._data.parsed,
    400    };
    401  }
    402 
    403  updateProgress() {
    404    debug`ProgressTracker updateProgress`;
    405 
    406    const data = this._data;
    407 
    408    if (!this._eventReceived || !data.uri) {
    409      return;
    410    }
    411 
    412    let progress = 0;
    413    if (data.pageStop || data.pageShow || data.completeProgress) {
    414      progress = 100;
    415    } else if (data.firstPaint) {
    416      progress = 80;
    417    } else if (data.parsed) {
    418      progress = 55;
    419    } else if (data.locationChange) {
    420      progress = 30;
    421    } else if (data.pageStart) {
    422      progress = 15;
    423    }
    424 
    425    if (data.prev >= progress) {
    426      return;
    427    }
    428 
    429    debug`ProgressTracker updateProgress data=${this._debugData()}
    430           progress=${progress}`;
    431 
    432    this.eventDispatcher.sendRequest({
    433      type: "GeckoView:ProgressChanged",
    434      progress,
    435    });
    436 
    437    data.prev = progress;
    438  }
    439 }
    440 
    441 class StateTracker extends Tracker {
    442  constructor(aModule) {
    443    super(aModule);
    444    this._inProgress = false;
    445    this._uri = null;
    446  }
    447 
    448  start(aUri) {
    449    this._inProgress = true;
    450    this._uri = aUri;
    451    this.eventDispatcher.sendRequest({
    452      type: "GeckoView:PageStart",
    453      uri: aUri,
    454    });
    455  }
    456 
    457  stop(aIsSuccess) {
    458    if (!this._inProgress) {
    459      // No request in progress
    460      return;
    461    }
    462 
    463    this._inProgress = false;
    464    this._uri = null;
    465 
    466    this.eventDispatcher.sendRequest({
    467      type: "GeckoView:PageStop",
    468      success: aIsSuccess,
    469    });
    470 
    471    lazy.BrowserTelemetryUtils.recordSiteOriginTelemetry(
    472      Services.wm.getEnumerator("navigator:geckoview"),
    473      true
    474    );
    475  }
    476 
    477  onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
    478    debug`StateTracker onStateChange: isTopLevel=${aWebProgress.isTopLevel},
    479                          flags=${aStateFlags}, status=${aStatus}
    480                          loadType=${aWebProgress.loadType}`;
    481 
    482    if (!aWebProgress.isTopLevel) {
    483      return;
    484    }
    485 
    486    const { displaySpec } = aRequest.QueryInterface(Ci.nsIChannel).URI;
    487    const isStart = (aStateFlags & Ci.nsIWebProgressListener.STATE_START) != 0;
    488    const isStop = (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) != 0;
    489 
    490    if (isStart) {
    491      this.start(displaySpec);
    492    } else if (isStop && !aWebProgress.isLoadingDocument) {
    493      this.stop(aStatus == Cr.NS_OK);
    494    }
    495  }
    496 }
    497 
    498 class SecurityTracker extends Tracker {
    499  constructor(aModule) {
    500    super(aModule);
    501    this._hostChanged = false;
    502  }
    503 
    504  onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
    505    debug`SecurityTracker onLocationChange: location=${aLocationURI.displaySpec},
    506                             flags=${aFlags}`;
    507 
    508    this._hostChanged = true;
    509  }
    510 
    511  onSecurityChange(aWebProgress, aRequest, aState) {
    512    debug`onSecurityChange`;
    513 
    514    // Don't need to do anything if the data we use to update the UI hasn't changed
    515    if (this._state === aState && !this._hostChanged) {
    516      return;
    517    }
    518 
    519    this._state = aState;
    520    this._hostChanged = false;
    521 
    522    const identity = IdentityHandler.checkIdentity(aState, this.browser);
    523 
    524    this.eventDispatcher.sendRequest({
    525      type: "GeckoView:SecurityChanged",
    526      identity,
    527    });
    528  }
    529 }
    530 
    531 export class GeckoViewProgress extends GeckoViewModule {
    532  onEnable() {
    533    debug`onEnable`;
    534 
    535    this._fireInitialLoad();
    536    this._initialAboutBlank = true;
    537 
    538    this._progressTracker = new ProgressTracker(this);
    539    this._securityTracker = new SecurityTracker(this);
    540    this._stateTracker = new StateTracker(this);
    541 
    542    const flags =
    543      Ci.nsIWebProgress.NOTIFY_STATE_NETWORK |
    544      Ci.nsIWebProgress.NOTIFY_SECURITY |
    545      Ci.nsIWebProgress.NOTIFY_LOCATION;
    546    this.progressFilter = Cc[
    547      "@mozilla.org/appshell/component/browser-status-filter;1"
    548    ].createInstance(Ci.nsIWebProgress);
    549    this.progressFilter.addProgressListener(this, flags);
    550    this.browser.addProgressListener(this.progressFilter, flags);
    551    Services.obs.addObserver(this, "oop-frameloader-crashed");
    552    this.registerListener("GeckoView:FlushSessionState");
    553  }
    554 
    555  onDisable() {
    556    debug`onDisable`;
    557 
    558    if (this.progressFilter) {
    559      this.progressFilter.removeProgressListener(this);
    560      this.browser.removeProgressListener(this.progressFilter);
    561    }
    562 
    563    Services.obs.removeObserver(this, "oop-frameloader-crashed");
    564    this.unregisterListener("GeckoView:FlushSessionState");
    565  }
    566 
    567  receiveMessage(aMsg) {
    568    debug`receiveMessage: ${aMsg.name}`;
    569 
    570    switch (aMsg.name) {
    571      case "DOMContentLoaded": // fall-through
    572      case "MozAfterPaint": // fall-through
    573      case "pageshow": {
    574        this._progressTracker?.handleEvent(aMsg);
    575        break;
    576      }
    577    }
    578  }
    579 
    580  onEvent(aEvent, aData) {
    581    debug`onEvent: event=${aEvent}, data=${aData}`;
    582 
    583    switch (aEvent) {
    584      case "GeckoView:FlushSessionState":
    585        this.messageManager.sendAsyncMessage("GeckoView:FlushSessionState");
    586        break;
    587    }
    588  }
    589 
    590  onStateChange(...args) {
    591    // GeckoView never gets PageStart or PageStop for about:blank because we
    592    // set nodefaultsrc to true unconditionally so we can assume here that
    593    // we're starting a page load for a non-blank page (or a consumer-initiated
    594    // about:blank load).
    595    this._initialAboutBlank = false;
    596 
    597    this._progressTracker.onStateChange(...args);
    598    this._stateTracker.onStateChange(...args);
    599  }
    600 
    601  onSecurityChange(...args) {
    602    // We don't report messages about the initial about:blank
    603    if (this._initialAboutBlank) {
    604      return;
    605    }
    606 
    607    this._securityTracker.onSecurityChange(...args);
    608  }
    609 
    610  onLocationChange(...args) {
    611    this._securityTracker.onLocationChange(...args);
    612    this._progressTracker.onLocationChange(...args);
    613  }
    614 
    615  // The initial about:blank load events are unreliable because docShell starts
    616  // up concurrently with loading geckoview.js so we're never guaranteed to get
    617  // the events.
    618  // What we do instead is ignore all initial about:blank events and fire them
    619  // manually once the child process has booted up.
    620  _fireInitialLoad() {
    621    this.eventDispatcher.sendRequest({
    622      type: "GeckoView:PageStart",
    623      uri: "about:blank",
    624    });
    625    this.eventDispatcher.sendRequest({
    626      type: "GeckoView:LocationChange",
    627      uri: "about:blank",
    628      canGoBack: false,
    629      canGoForward: false,
    630      isTopLevel: true,
    631      hasUserGesture: false,
    632    });
    633    this.eventDispatcher.sendRequest({
    634      type: "GeckoView:PageStop",
    635      success: true,
    636    });
    637  }
    638 
    639  // nsIObserver event handler
    640  observe(aSubject, aTopic) {
    641    debug`observe: topic=${aTopic}`;
    642 
    643    switch (aTopic) {
    644      case "oop-frameloader-crashed": {
    645        const browser = aSubject.ownerElement;
    646        if (!browser || browser != this.browser) {
    647          return;
    648        }
    649 
    650        this._progressTracker?.stop(/* isSuccess */ false);
    651        this._stateTracker?.stop(/* isSuccess */ false);
    652      }
    653    }
    654  }
    655 }
    656 
    657 const { debug, warn } = GeckoViewProgress.initLogging("GeckoViewProgress");