tor-browser

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

browser-captivePortal.js (13032B)


      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 http://mozilla.org/MPL/2.0/. */
      4 
      5 var CaptivePortalWatcher = {
      6  // This is the value used to identify the captive portal notification.
      7  PORTAL_NOTIFICATION_VALUE: "captive-portal-detected",
      8 
      9  // This holds a weak reference to the captive portal tab so that we
     10  // don't leak it if the user closes it.
     11  _captivePortalTab: null,
     12 
     13  /**
     14   * If a portal is detected when we don't have focus, we first wait for focus
     15   * and then add the tab if, after a recheck, the portal is still active. This
     16   * is set to true while we wait so that in the unlikely event that we receive
     17   * another notification while waiting, we don't do things twice.
     18   */
     19  _delayedCaptivePortalDetectedInProgress: false,
     20 
     21  // In the situation above, this is set to true while we wait for the recheck.
     22  // This flag exists so that tests can appropriately simulate a recheck.
     23  _waitingForRecheck: false,
     24 
     25  // This holds a weak reference to the captive portal tab so we can close the tab
     26  // after successful login if we're redirected to the canonicalURL.
     27  _previousCaptivePortalTab: null,
     28 
     29  // Stores the time at which the banner was displayed
     30  _bannerDisplayTime: Date.now(),
     31 
     32  get _captivePortalNotification() {
     33    return gNotificationBox.getNotificationWithValue(
     34      this.PORTAL_NOTIFICATION_VALUE
     35    );
     36  },
     37 
     38  get canonicalURL() {
     39    return Services.prefs.getCharPref("captivedetect.canonicalURL");
     40  },
     41 
     42  get _browserBundle() {
     43    delete this._browserBundle;
     44    return (this._browserBundle = Services.strings.createBundle(
     45      "chrome://browser/locale/browser.properties"
     46    ));
     47  },
     48 
     49  init() {
     50    Services.obs.addObserver(this, "captive-portal-login");
     51    Services.obs.addObserver(this, "captive-portal-login-abort");
     52    Services.obs.addObserver(this, "captive-portal-login-success");
     53 
     54    this._cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
     55      Ci.nsICaptivePortalService
     56    );
     57 
     58    if (this._cps.state == this._cps.LOCKED_PORTAL) {
     59      // A captive portal has already been detected.
     60      this._captivePortalDetected();
     61 
     62      // Automatically open a captive portal tab if there's no other browser window.
     63      if (BrowserWindowTracker.windowCount == 1) {
     64        this.ensureCaptivePortalTab();
     65      }
     66    } else if (this._cps.state == this._cps.UNKNOWN) {
     67      // We trigger a portal check after delayed startup to avoid doing a network
     68      // request before first paint.
     69      this._delayedRecheckPending = true;
     70    }
     71 
     72    // This constant is chosen to be large enough for a portal recheck to complete,
     73    // and small enough that the delay in opening a tab isn't too noticeable.
     74    // Please see comments for _delayedCaptivePortalDetected for more details.
     75    XPCOMUtils.defineLazyPreferenceGetter(
     76      this,
     77      "PORTAL_RECHECK_DELAY_MS",
     78      "captivedetect.portalRecheckDelayMS",
     79      500
     80    );
     81  },
     82 
     83  uninit() {
     84    Services.obs.removeObserver(this, "captive-portal-login");
     85    Services.obs.removeObserver(this, "captive-portal-login-abort");
     86    Services.obs.removeObserver(this, "captive-portal-login-success");
     87 
     88    this._cancelDelayedCaptivePortal();
     89  },
     90 
     91  delayedStartup() {
     92    if (this._delayedRecheckPending) {
     93      delete this._delayedRecheckPending;
     94      this._cps.recheckCaptivePortal();
     95    }
     96  },
     97 
     98  observe(aSubject, aTopic) {
     99    switch (aTopic) {
    100      case "captive-portal-login":
    101        this._captivePortalDetected();
    102        break;
    103      case "captive-portal-login-abort":
    104        this._captivePortalGone(false);
    105        break;
    106      case "captive-portal-login-success":
    107        this._captivePortalGone(true);
    108        break;
    109      case "delayed-captive-portal-handled":
    110        this._cancelDelayedCaptivePortal();
    111        break;
    112    }
    113  },
    114 
    115  onLocationChange(browser) {
    116    if (!this._previousCaptivePortalTab) {
    117      return;
    118    }
    119 
    120    let tab = this._previousCaptivePortalTab.get();
    121    if (!tab || !tab.linkedBrowser) {
    122      return;
    123    }
    124 
    125    if (browser != tab.linkedBrowser) {
    126      return;
    127    }
    128 
    129    // There is a race between the release of captive portal i.e.
    130    // the time when success/abort events are fired and the time when
    131    // the captive portal tab redirects to the canonicalURL. We check for
    132    // both conditions to be true and also check that we haven't already removed
    133    // the captive portal tab in the success/abort event handlers before we remove
    134    // it in the callback below. A tick is added to avoid removing the tab before
    135    // onLocationChange handlers across browser code are executed.
    136    Services.tm.dispatchToMainThread(() => {
    137      if (!this._previousCaptivePortalTab) {
    138        return;
    139      }
    140 
    141      tab = this._previousCaptivePortalTab.get();
    142      let canonicalURI = Services.io.newURI(this.canonicalURL);
    143      if (
    144        tab &&
    145        (tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI) ||
    146          tab.linkedBrowser.currentURI.host == "support.mozilla.org") &&
    147        (this._cps.state == this._cps.UNLOCKED_PORTAL ||
    148          this._cps.state == this._cps.UNKNOWN)
    149      ) {
    150        gBrowser.removeTab(tab);
    151      }
    152    });
    153  },
    154 
    155  _captivePortalDetected() {
    156    if (this._delayedCaptivePortalDetectedInProgress) {
    157      return;
    158    }
    159 
    160    // Add an explicit permission for the last detected URI such that https-only / https-first do not
    161    // attempt to upgrade the URI to https when following the "open network login page" button.
    162    // We set explicit permissions for regular and private browsing windows to keep permissions
    163    // separate.
    164    let canonicalURI = Services.io.newURI(this.canonicalURL);
    165    let isPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
    166    let principal = Services.scriptSecurityManager.createContentPrincipal(
    167      canonicalURI,
    168      {
    169        userContextId: gBrowser.contentPrincipal.userContextId,
    170        privateBrowsingId: isPrivate ? 1 : 0,
    171      }
    172    );
    173    Services.perms.addFromPrincipal(
    174      principal,
    175      "https-only-load-insecure",
    176      Ci.nsIPermissionManager.ALLOW_ACTION,
    177      Ci.nsIPermissionManager.EXPIRE_SESSION
    178    );
    179    let win = BrowserWindowTracker.getTopWindow();
    180    // Used by tests: ignore the main test window in order to enable testing of
    181    // the case where we have no open windows.
    182    if (win?.document.documentElement.getAttribute("ignorecaptiveportal")) {
    183      win = null;
    184    }
    185 
    186    // If no browser window has focus, open and show the tab when we regain focus.
    187    // This is so that if a different application was focused, when the user
    188    // (re-)focuses a browser window, we open the tab immediately in that window
    189    // so they can log in before continuing to browse.
    190    if (win != Services.focus.activeWindow) {
    191      this._delayedCaptivePortalDetectedInProgress = true;
    192      window.addEventListener("activate", this, { once: true });
    193      Services.obs.addObserver(this, "delayed-captive-portal-handled");
    194    }
    195 
    196    this._showNotification();
    197  },
    198 
    199  /**
    200   * Called after we regain focus if we detect a portal while a browser window
    201   * doesn't have focus. Triggers a portal recheck to reaffirm state, and adds
    202   * the tab if needed after a short delay to allow the recheck to complete.
    203   */
    204  _delayedCaptivePortalDetected() {
    205    if (!this._delayedCaptivePortalDetectedInProgress) {
    206      return;
    207    }
    208 
    209    // Used by tests: ignore the main test window in order to enable testing of
    210    // the case where we have no open windows.
    211    if (window.document.documentElement.getAttribute("ignorecaptiveportal")) {
    212      return;
    213    }
    214 
    215    Services.obs.notifyObservers(null, "delayed-captive-portal-handled");
    216 
    217    // Trigger a portal recheck. The user may have logged into the portal via
    218    // another client, or changed networks.
    219    this._cps.recheckCaptivePortal();
    220    this._waitingForRecheck = true;
    221    let requestTime = Date.now();
    222 
    223    let observer = () => {
    224      let time = Date.now() - requestTime;
    225      Services.obs.removeObserver(observer, "captive-portal-check-complete");
    226      this._waitingForRecheck = false;
    227      if (this._cps.state != this._cps.LOCKED_PORTAL) {
    228        // We're free of the portal!
    229        return;
    230      }
    231 
    232      if (time <= this.PORTAL_RECHECK_DELAY_MS) {
    233        // The amount of time elapsed since we requested a recheck (i.e. since
    234        // the browser window was focused) was small enough that we can add and
    235        // focus a tab with the login page with no noticeable delay.
    236        this.ensureCaptivePortalTab();
    237      }
    238    };
    239    Services.obs.addObserver(observer, "captive-portal-check-complete");
    240  },
    241 
    242  _captivePortalGone(aSuccess) {
    243    this._cancelDelayedCaptivePortal();
    244    this._removeNotification();
    245 
    246    let durationInSeconds = Math.round(
    247      (Date.now() - this._bannerDisplayTime) / 1000
    248    );
    249 
    250    if (aSuccess) {
    251      Glean.networking.captivePortalBannerDisplayTime.success.add(
    252        durationInSeconds
    253      );
    254    } else {
    255      Glean.networking.captivePortalBannerDisplayTime.abort.add(
    256        durationInSeconds
    257      );
    258    }
    259 
    260    if (!this._captivePortalTab) {
    261      return;
    262    }
    263 
    264    let tab = this._captivePortalTab.get();
    265    let canonicalURI = Services.io.newURI(this.canonicalURL);
    266    if (
    267      tab &&
    268      tab.linkedBrowser &&
    269      (tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI) ||
    270        tab.linkedBrowser.currentURI.host == "support.mozilla.org")
    271    ) {
    272      this._previousCaptivePortalTab = null;
    273      gBrowser.removeTab(tab);
    274    }
    275    this._captivePortalTab = null;
    276  },
    277 
    278  _cancelDelayedCaptivePortal() {
    279    if (this._delayedCaptivePortalDetectedInProgress) {
    280      this._delayedCaptivePortalDetectedInProgress = false;
    281      Services.obs.removeObserver(this, "delayed-captive-portal-handled");
    282      window.removeEventListener("activate", this);
    283    }
    284  },
    285 
    286  async handleEvent(aEvent) {
    287    switch (aEvent.type) {
    288      case "activate":
    289        this._delayedCaptivePortalDetected();
    290        break;
    291      case "TabSelect": {
    292        if (this._notificationPromise) {
    293          await this._notificationPromise;
    294        }
    295        if (!this._captivePortalTab || !this._captivePortalNotification) {
    296          break;
    297        }
    298 
    299        let tab = this._captivePortalTab.get();
    300        let n = this._captivePortalNotification;
    301        if (!tab || !n) {
    302          break;
    303        }
    304 
    305        let doc = tab.ownerDocument;
    306        let button = n.buttonContainer.querySelector(
    307          "button.notification-button"
    308        );
    309        if (doc.defaultView.gBrowser.selectedTab == tab) {
    310          button.style.visibility = "hidden";
    311        } else {
    312          button.style.visibility = "visible";
    313        }
    314        break;
    315      }
    316    }
    317  },
    318 
    319  _showNotification() {
    320    if (this._captivePortalNotification) {
    321      return;
    322    }
    323 
    324    Glean.networking.captivePortalBannerDisplayed.add(1);
    325    this._bannerDisplayTime = Date.now();
    326 
    327    let buttons = [
    328      {
    329        label: this._browserBundle.GetStringFromName(
    330          "captivePortal.showLoginPage2"
    331        ),
    332        callback: () => {
    333          this.ensureCaptivePortalTab();
    334 
    335          // Returning true prevents the notification from closing.
    336          return true;
    337        },
    338      },
    339    ];
    340 
    341    let message = this._browserBundle.GetStringFromName(
    342      "captivePortal.infoMessage3"
    343    );
    344 
    345    let closeHandler = aEventName => {
    346      if (aEventName == "dismissed") {
    347        let durationInSeconds = Math.round(
    348          (Date.now() - this._bannerDisplayTime) / 1000
    349        );
    350 
    351        Glean.networking.captivePortalBannerDisplayTime.dismiss.add(
    352          durationInSeconds
    353        );
    354      }
    355 
    356      if (aEventName != "removed") {
    357        return;
    358      }
    359      gBrowser.tabContainer.removeEventListener("TabSelect", this);
    360    };
    361 
    362    this._notificationPromise = gNotificationBox.appendNotification(
    363      this.PORTAL_NOTIFICATION_VALUE,
    364      {
    365        label: message,
    366        priority: gNotificationBox.PRIORITY_INFO_MEDIUM,
    367        eventCallback: closeHandler,
    368      },
    369      buttons
    370    );
    371 
    372    gBrowser.tabContainer.addEventListener("TabSelect", this);
    373  },
    374 
    375  _removeNotification() {
    376    let n = this._captivePortalNotification;
    377    if (!n || !n.parentNode) {
    378      return;
    379    }
    380    n.close();
    381  },
    382 
    383  ensureCaptivePortalTab() {
    384    let tab;
    385    if (this._captivePortalTab) {
    386      tab = this._captivePortalTab.get();
    387    }
    388 
    389    // If the tab is gone or going, we need to open a new one.
    390    if (!tab || tab.closing || !tab.parentNode) {
    391      tab = gBrowser.addWebTab(this.canonicalURL, {
    392        ownerTab: gBrowser.selectedTab,
    393        triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
    394          {
    395            userContextId: gBrowser.contentPrincipal.userContextId,
    396          }
    397        ),
    398        isCaptivePortalTab: true,
    399      });
    400      this._captivePortalTab = Cu.getWeakReference(tab);
    401      this._previousCaptivePortalTab = Cu.getWeakReference(tab);
    402    }
    403 
    404    gBrowser.selectedTab = tab;
    405  },
    406 };