Pier Angelo Vendrame pushed to branch tor-browser-115.9.0esr-13.5-1 at The Tor Project / Applications / Tor Browser

Commits:

8 changed files:

Changes:

  • browser/components/torpreferences/content/connectionPane.js
    ... ... @@ -36,7 +36,7 @@ const { TorStrings } = ChromeUtils.importESModule(
    36 36
       "resource://gre/modules/TorStrings.sys.mjs"
    
    37 37
     );
    
    38 38
     
    
    39
    -const { Lox } = ChromeUtils.importESModule(
    
    39
    +const { Lox, LoxTopics } = ChromeUtils.importESModule(
    
    40 40
       "resource://gre/modules/Lox.sys.mjs"
    
    41 41
     );
    
    42 42
     
    
    ... ... @@ -1319,27 +1319,18 @@ const gLoxStatus = {
    1319 1319
         this._invitesButton.addEventListener("click", () => {
    
    1320 1320
           gSubDialog.open(
    
    1321 1321
             "chrome://browser/content/torpreferences/loxInviteDialog.xhtml",
    
    1322
    -        {
    
    1323
    -          features: "resizable=yes",
    
    1324
    -          closedCallback: () => {
    
    1325
    -            // TODO: Listen for events from Lox, rather than call _updateInvites
    
    1326
    -            // directly.
    
    1327
    -            this._updateInvites();
    
    1328
    -          },
    
    1329
    -        }
    
    1322
    +        { features: "resizable=yes" }
    
    1330 1323
           );
    
    1331 1324
         });
    
    1332 1325
         this._unlockAlertButton.addEventListener("click", () => {
    
    1333
    -      // TODO: Have a way to ensure that the cleared event data matches the
    
    1334
    -      // current _loxId
    
    1335
    -      Lox.clearEventData();
    
    1336
    -      // TODO: Listen for events from Lox, rather than call _updateUnlocks
    
    1337
    -      // directly.
    
    1338
    -      this._updateUnlocks();
    
    1326
    +      Lox.clearEventData(this._loxId);
    
    1339 1327
         });
    
    1340 1328
     
    
    1341 1329
         Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
    
    1342
    -    // TODO: Listen for new events from Lox, when it is supported.
    
    1330
    +    Services.obs.addObserver(this, LoxTopics.UpdateEvents);
    
    1331
    +    Services.obs.addObserver(this, LoxTopics.UpdateNextUnlock);
    
    1332
    +    Services.obs.addObserver(this, LoxTopics.UpdateRemainingInvites);
    
    1333
    +    Services.obs.addObserver(this, LoxTopics.NewInvite);
    
    1343 1334
     
    
    1344 1335
         // NOTE: Before initializedPromise completes, this area is hidden.
    
    1345 1336
         TorSettings.initializedPromise.then(() => {
    
    ... ... @@ -1352,6 +1343,10 @@ const gLoxStatus = {
    1352 1343
        */
    
    1353 1344
       uninit() {
    
    1354 1345
         Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    
    1346
    +    Services.obs.removeObserver(this, LoxTopics.UpdateEvents);
    
    1347
    +    Services.obs.removeObserver(this, LoxTopics.UpdateNextUnlock);
    
    1348
    +    Services.obs.removeObserver(this, LoxTopics.UpdateRemainingInvites);
    
    1349
    +    Services.obs.removeObserver(this, LoxTopics.NewInvite);
    
    1355 1350
       },
    
    1356 1351
     
    
    1357 1352
       observe(subject, topic, data) {
    
    ... ... @@ -1365,6 +1360,18 @@ const gLoxStatus = {
    1365 1360
               this._updateLoxId();
    
    1366 1361
             }
    
    1367 1362
             break;
    
    1363
    +      case LoxTopics.UpdateNextUnlock:
    
    1364
    +        this._updateNextUnlock();
    
    1365
    +        break;
    
    1366
    +      case LoxTopics.UpdateEvents:
    
    1367
    +        this._updatePendingEvents();
    
    1368
    +        break;
    
    1369
    +      case LoxTopics.UpdateRemainingInvites:
    
    1370
    +        this._updateRemainingInvites();
    
    1371
    +        break;
    
    1372
    +      case LoxTopics.NewInvite:
    
    1373
    +        this._updateHaveExistingInvites();
    
    1374
    +        break;
    
    1368 1375
         }
    
    1369 1376
       },
    
    1370 1377
     
    
    ... ... @@ -1384,43 +1391,126 @@ const gLoxStatus = {
    1384 1391
           TorSettings.bridges.source === TorBridgeSource.Lox
    
    1385 1392
             ? TorSettings.bridges.lox_id
    
    1386 1393
             : "";
    
    1387
    -    if (loxId !== this._loxId) {
    
    1388
    -      this._loxId = loxId;
    
    1389
    -      this._updateUnlocks();
    
    1390
    -      this._updateInvites();
    
    1394
    +    if (loxId === this._loxId) {
    
    1395
    +      return;
    
    1391 1396
         }
    
    1397
    +    this._loxId = loxId;
    
    1398
    +    // We unset _nextUnlock to ensure the areas no longer use the old value for
    
    1399
    +    // the new loxId.
    
    1400
    +    this._updateNextUnlock(true);
    
    1401
    +    this._updateRemainingInvites();
    
    1402
    +    this._updateHaveExistingInvites();
    
    1403
    +    this._updatePendingEvents();
    
    1392 1404
       },
    
    1393 1405
     
    
    1394 1406
       /**
    
    1395
    -   * Update the display of the current or next unlock.
    
    1407
    +   * The remaining invites shown, or null if uninitialized or no loxId.
    
    1408
    +   *
    
    1409
    +   * @type {integer?}
    
    1396 1410
        */
    
    1397
    -  async _updateUnlocks() {
    
    1398
    -    // Cache the loxId before we await.
    
    1399
    -    const loxId = this._loxId;
    
    1400
    -
    
    1401
    -    if (!loxId) {
    
    1402
    -      // NOTE: This area should already be hidden by the change in Lox source,
    
    1403
    -      // but we clean up for the next non-empty id.
    
    1404
    -      this._area.classList.remove("show-unlock-alert");
    
    1405
    -      this._area.classList.remove("show-next-unlock");
    
    1411
    +  _remainingInvites: null,
    
    1412
    +  /**
    
    1413
    +   * Update the shown value.
    
    1414
    +   */
    
    1415
    +  _updateRemainingInvites() {
    
    1416
    +    const numInvites = this._loxId
    
    1417
    +      ? Lox.getRemainingInviteCount(this._loxId)
    
    1418
    +      : null;
    
    1419
    +    if (numInvites === this._remainingInvites) {
    
    1406 1420
           return;
    
    1407 1421
         }
    
    1408
    -
    
    1409
    -    let pendingEvents;
    
    1410
    -    let nextUnlock;
    
    1411
    -    let numInvites;
    
    1412
    -    // Fetch the latest events or details about the next unlock.
    
    1413
    -    try {
    
    1414
    -      nextUnlock = await Lox.getNextUnlock();
    
    1415
    -      pendingEvents = Lox.getEventData();
    
    1416
    -      numInvites = Lox.getRemainingInviteCount();
    
    1417
    -    } catch (e) {
    
    1418
    -      console.error("Failed get get lox updates", e);
    
    1422
    +    this._remainingInvites = numInvites;
    
    1423
    +    this._updateUnlockArea();
    
    1424
    +    this._updateInvitesArea();
    
    1425
    +  },
    
    1426
    +  /**
    
    1427
    +   * Whether we have existing invites, or null if uninitialized or no loxId.
    
    1428
    +   *
    
    1429
    +   * @type {boolean?}
    
    1430
    +   */
    
    1431
    +  _haveExistingInvites: null,
    
    1432
    +  /**
    
    1433
    +   * Update the shown value.
    
    1434
    +   */
    
    1435
    +  _updateHaveExistingInvites() {
    
    1436
    +    const haveInvites = this._loxId ? !!Lox.getInvites().length : null;
    
    1437
    +    if (haveInvites === this._haveExistingInvites) {
    
    1438
    +      return;
    
    1439
    +    }
    
    1440
    +    this._haveExistingInvites = haveInvites;
    
    1441
    +    this._updateInvitesArea();
    
    1442
    +  },
    
    1443
    +  /**
    
    1444
    +   * Details about the next unlock, or null if uninitialized or no loxId.
    
    1445
    +   *
    
    1446
    +   * @type {UnlockData?}
    
    1447
    +   */
    
    1448
    +  _nextUnlock: null,
    
    1449
    +  /**
    
    1450
    +   * Tracker id to ensure that the results from later calls to _updateNextUnlock
    
    1451
    +   * take priority over earlier calls.
    
    1452
    +   *
    
    1453
    +   * @type {integer}
    
    1454
    +   */
    
    1455
    +  _nextUnlockCallId: 0,
    
    1456
    +  /**
    
    1457
    +   * Update the shown value asynchronously.
    
    1458
    +   *
    
    1459
    +   * @param {boolean} [unset=false] - Whether to set the _nextUnlock value to
    
    1460
    +   *   null before waiting for the new value. I.e. ensure that the current value
    
    1461
    +   *   will not be used.
    
    1462
    +   */
    
    1463
    +  async _updateNextUnlock(unset = false) {
    
    1464
    +    // NOTE: We do not expect the integer to exceed the maximum integer.
    
    1465
    +    this._nextUnlockCallId++;
    
    1466
    +    const callId = this._nextUnlockCallId;
    
    1467
    +    if (unset) {
    
    1468
    +      this._nextUnlock = null;
    
    1469
    +    }
    
    1470
    +    const nextUnlock = this._loxId
    
    1471
    +      ? await Lox.getNextUnlock(this._loxId)
    
    1472
    +      : null;
    
    1473
    +    if (callId !== this._nextUnlockCallId) {
    
    1474
    +      // Replaced by another update.
    
    1475
    +      // E.g. if the _loxId changed. Or if getNextUnlock triggered
    
    1476
    +      // LoxTopics.UpdateNextUnlock.
    
    1419 1477
           return;
    
    1420 1478
         }
    
    1479
    +    // Should be safe to trigger the update, even when the value hasn't changed.
    
    1480
    +    this._nextUnlock = nextUnlock;
    
    1481
    +    this._updateUnlockArea();
    
    1482
    +  },
    
    1483
    +  /**
    
    1484
    +   * The list of events the user has not yet cleared, or null if uninitialized
    
    1485
    +   * or no loxId.
    
    1486
    +   *
    
    1487
    +   * @type {EventData[]?}
    
    1488
    +   */
    
    1489
    +  _pendingEvents: null,
    
    1490
    +  /**
    
    1491
    +   * Update the shown value.
    
    1492
    +   */
    
    1493
    +  _updatePendingEvents() {
    
    1494
    +    // Should be safe to trigger the update, even when the value hasn't changed.
    
    1495
    +    this._pendingEvents = this._loxId ? Lox.getEventData(this._loxId) : null;
    
    1496
    +    this._updateUnlockArea();
    
    1497
    +  },
    
    1421 1498
     
    
    1422
    -    if (loxId !== this._loxId) {
    
    1423
    -      // Replaced during await.
    
    1499
    +  /**
    
    1500
    +   * Update the display of the current or next unlock.
    
    1501
    +   */
    
    1502
    +  _updateUnlockArea() {
    
    1503
    +    if (
    
    1504
    +      !this._loxId ||
    
    1505
    +      this._pendingEvents === null ||
    
    1506
    +      this._remainingInvites === null ||
    
    1507
    +      this._nextUnlock === null
    
    1508
    +    ) {
    
    1509
    +      // Uninitialized or no Lox source.
    
    1510
    +      // NOTE: This area may already be hidden by the change in Lox source,
    
    1511
    +      // but we clean up for the next non-empty id.
    
    1512
    +      this._area.classList.remove("show-unlock-alert");
    
    1513
    +      this._area.classList.remove("show-next-unlock");
    
    1424 1514
           return;
    
    1425 1515
         }
    
    1426 1516
     
    
    ... ... @@ -1428,6 +1518,7 @@ const gLoxStatus = {
    1428 1518
         const alertHadFocus = this._unlockAlert.contains(document.activeElement);
    
    1429 1519
         const detailsHadFocus = this._detailsArea.contains(document.activeElement);
    
    1430 1520
     
    
    1521
    +    const pendingEvents = this._pendingEvents;
    
    1431 1522
         const showAlert = !!pendingEvents.length;
    
    1432 1523
         this._area.classList.toggle("show-unlock-alert", showAlert);
    
    1433 1524
         this._area.classList.toggle("show-next-unlock", !showAlert);
    
    ... ... @@ -1479,7 +1570,7 @@ const gLoxStatus = {
    1479 1570
           document.l10n.setAttributes(
    
    1480 1571
             this._unlockAlertInviteItem,
    
    1481 1572
             "tor-bridges-lox-new-invites",
    
    1482
    -        { numInvites }
    
    1573
    +        { numInvites: this._remainingInvites }
    
    1483 1574
           );
    
    1484 1575
           this._unlockAlert.classList.toggle(
    
    1485 1576
             "lox-unlock-upgrade",
    
    ... ... @@ -1494,7 +1585,7 @@ const gLoxStatus = {
    1494 1585
           const numDays = Math.max(
    
    1495 1586
             1,
    
    1496 1587
             Math.ceil(
    
    1497
    -          (new Date(nextUnlock.date).getTime() - Date.now()) /
    
    1588
    +          (new Date(this._nextUnlock.date).getTime() - Date.now()) /
    
    1498 1589
                 (24 * 60 * 60 * 1000)
    
    1499 1590
             )
    
    1500 1591
           );
    
    ... ... @@ -1505,9 +1596,9 @@ const gLoxStatus = {
    1505 1596
           );
    
    1506 1597
     
    
    1507 1598
           // Gain 2 bridges from level 0 to 1. After that gain invites.
    
    1508
    -      const bridgeGain = nextUnlock.nextLevel === 1;
    
    1509
    -      const firstInvites = nextUnlock.nextLevel === 2;
    
    1510
    -      const moreInvites = nextUnlock.nextLevel > 2;
    
    1599
    +      const bridgeGain = this._nextUnlock.nextLevel === 1;
    
    1600
    +      const firstInvites = this._nextUnlock.nextLevel === 2;
    
    1601
    +      const moreInvites = this._nextUnlock.nextLevel > 2;
    
    1511 1602
     
    
    1512 1603
           this._detailsArea.classList.toggle("lox-next-gain-bridges", bridgeGain);
    
    1513 1604
           this._detailsArea.classList.toggle(
    
    ... ... @@ -1529,24 +1620,19 @@ const gLoxStatus = {
    1529 1620
       /**
    
    1530 1621
        * Update the invites area.
    
    1531 1622
        */
    
    1532
    -  _updateInvites() {
    
    1533
    -    if (!this._loxId) {
    
    1534
    -      return;
    
    1535
    -    }
    
    1536
    -
    
    1537
    -    let remainingInvites;
    
    1538
    -    let existingInvites;
    
    1539
    -    // Fetch the latest events or details about the next unlock.
    
    1540
    -    try {
    
    1541
    -      remainingInvites = Lox.getRemainingInviteCount();
    
    1542
    -      existingInvites = Lox.getInvites().length;
    
    1543
    -    } catch (e) {
    
    1544
    -      console.error("Failed get get remaining invites", e);
    
    1545
    -      return;
    
    1623
    +  _updateInvitesArea() {
    
    1624
    +    let hasInvites;
    
    1625
    +    if (
    
    1626
    +      !this._loxId ||
    
    1627
    +      this._remainingInvites === null ||
    
    1628
    +      this._haveExistingInvites === null
    
    1629
    +    ) {
    
    1630
    +      // Not initialized yet.
    
    1631
    +      hasInvites = false;
    
    1632
    +    } else {
    
    1633
    +      hasInvites = this._haveExistingInvites || !!this._remainingInvites;
    
    1546 1634
         }
    
    1547 1635
     
    
    1548
    -    const hasInvites = !!existingInvites || !!remainingInvites;
    
    1549
    -
    
    1550 1636
         if (!hasInvites) {
    
    1551 1637
           if (
    
    1552 1638
             this._remainingInvitesEl.contains(document.activeElement) ||
    
    ... ... @@ -1563,11 +1649,13 @@ const gLoxStatus = {
    1563 1649
         // creating new ones.
    
    1564 1650
         this._detailsArea.classList.toggle("lox-has-invites", hasInvites);
    
    1565 1651
     
    
    1566
    -    document.l10n.setAttributes(
    
    1567
    -      this._remainingInvitesEl,
    
    1568
    -      "tor-bridges-lox-remaining-invites",
    
    1569
    -      { numInvites: remainingInvites }
    
    1570
    -    );
    
    1652
    +    if (hasInvites) {
    
    1653
    +      document.l10n.setAttributes(
    
    1654
    +        this._remainingInvitesEl,
    
    1655
    +        "tor-bridges-lox-remaining-invites",
    
    1656
    +        { numInvites: this._remainingInvites }
    
    1657
    +      );
    
    1658
    +    }
    
    1571 1659
       },
    
    1572 1660
     };
    
    1573 1661
     
    

  • browser/components/torpreferences/content/loxInviteDialog.js
    ... ... @@ -3,14 +3,14 @@
    3 3
     const { TorSettings, TorSettingsTopics, TorBridgeSource } =
    
    4 4
       ChromeUtils.importESModule("resource://gre/modules/TorSettings.sys.mjs");
    
    5 5
     
    
    6
    -const { Lox, LoxErrors } = ChromeUtils.importESModule(
    
    6
    +const { Lox, LoxError, LoxTopics } = ChromeUtils.importESModule(
    
    7 7
       "resource://gre/modules/Lox.sys.mjs"
    
    8 8
     );
    
    9 9
     
    
    10 10
     /**
    
    11 11
      * Fake Lox module
    
    12 12
     
    
    13
    -const LoxErrors = {
    
    13
    +const LoxError = {
    
    14 14
       LoxServerUnreachable: "LoxServerUnreachable",
    
    15 15
       Other: "Other",
    
    16 16
     };
    
    ... ... @@ -36,7 +36,7 @@ const Lox = {
    36 36
               return;
    
    37 37
             }
    
    38 38
             if (!this.remainingInvites) {
    
    39
    -          rej({ type: LoxErrors.Other });
    
    39
    +          rej({ type: LoxError.Other });
    
    40 40
               return;
    
    41 41
             }
    
    42 42
             const invite = JSON.stringify({
    
    ... ... @@ -104,7 +104,8 @@ const gLoxInvites = {
    104 104
         // NOTE: TorSettings should already be initialized when this dialog is
    
    105 105
         // opened.
    
    106 106
         Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
    
    107
    -    // TODO: Listen for new invites from Lox, when supported.
    
    107
    +    Services.obs.addObserver(this, LoxTopics.UpdateRemainingInvites);
    
    108
    +    Services.obs.addObserver(this, LoxTopics.NewInvite);
    
    108 109
     
    
    109 110
         // Set initial _loxId value. Can close this dialog.
    
    110 111
         this._updateLoxId();
    
    ... ... @@ -118,6 +119,8 @@ const gLoxInvites = {
    118 119
        */
    
    119 120
       uninit() {
    
    120 121
         Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
    
    122
    +    Services.obs.removeObserver(this, LoxTopics.UpdateRemainingInvites);
    
    123
    +    Services.obs.removeObserver(this, LoxTopics.NewInvite);
    
    121 124
       },
    
    122 125
     
    
    123 126
       observe(subject, topic, data) {
    
    ... ... @@ -131,6 +134,12 @@ const gLoxInvites = {
    131 134
               this._updateLoxId();
    
    132 135
             }
    
    133 136
             break;
    
    137
    +      case LoxTopics.UpdateRemainingInvites:
    
    138
    +        this._updateRemainingInvites();
    
    139
    +        break;
    
    140
    +      case LoxTopics.NewInvite:
    
    141
    +        this._updateExistingInvites();
    
    142
    +        break;
    
    134 143
         }
    
    135 144
       },
    
    136 145
     
    
    ... ... @@ -204,7 +213,7 @@ const gLoxInvites = {
    204 213
        * Update the display of the remaining invites.
    
    205 214
        */
    
    206 215
       _updateRemainingInvites() {
    
    207
    -    this._remainingInvites = Lox.getRemainingInviteCount();
    
    216
    +    this._remainingInvites = Lox.getRemainingInviteCount(this._loxId);
    
    208 217
     
    
    209 218
         document.l10n.setAttributes(
    
    210 219
           this._remainingInvitesEl,
    
    ... ... @@ -254,7 +263,7 @@ const gLoxInvites = {
    254 263
         this._connectingEl.focus();
    
    255 264
     
    
    256 265
         let lostFocus = false;
    
    257
    -    Lox.generateInvite()
    
    266
    +    Lox.generateInvite(this._loxId)
    
    258 267
           .finally(() => {
    
    259 268
             // Fetch whether the connecting label still has focus before we hide it.
    
    260 269
             lostFocus = this._connectingEl.contains(document.activeElement);
    
    ... ... @@ -275,15 +284,11 @@ const gLoxInvites = {
    275 284
                 // message.
    
    276 285
                 this._inviteListEl.focus();
    
    277 286
               }
    
    278
    -
    
    279
    -          // TODO: When Lox sends out notifications, let the observer handle the
    
    280
    -          // change rather than calling _updateRemainingInvites directly.
    
    281
    -          this._updateRemainingInvites();
    
    282 287
             },
    
    283 288
             loxError => {
    
    284 289
               console.error("Failed to generate an invite", loxError);
    
    285
    -          switch (loxError.type) {
    
    286
    -            case LoxErrors.LoxServerUnreachable:
    
    290
    +          switch (loxError instanceof LoxError ? loxError.code : null) {
    
    291
    +            case LoxError.LoxServerUnreachable:
    
    287 292
                   this._updateGenerateError("no-server");
    
    288 293
                   break;
    
    289 294
                 default:
    

  • browser/components/torpreferences/content/provideBridgeDialog.js
    ... ... @@ -15,14 +15,14 @@ const { TorParsers } = ChromeUtils.importESModule(
    15 15
       "resource://gre/modules/TorParsers.sys.mjs"
    
    16 16
     );
    
    17 17
     
    
    18
    -const { Lox, LoxErrors } = ChromeUtils.importESModule(
    
    18
    +const { Lox, LoxError } = ChromeUtils.importESModule(
    
    19 19
       "resource://gre/modules/Lox.sys.mjs"
    
    20 20
     );
    
    21 21
     
    
    22 22
     /*
    
    23 23
      * Fake Lox module:
    
    24 24
     
    
    25
    -const LoxErrors = {
    
    25
    +const LoxError = {
    
    26 26
       BadInvite: "BadInvite",
    
    27 27
       LoxServerUnreachable: "LoxServerUnreachable",
    
    28 28
       Other: "Other",
    
    ... ... @@ -30,9 +30,9 @@ const LoxErrors = {
    30 30
     
    
    31 31
     const Lox = {
    
    32 32
       failError: null,
    
    33
    -  // failError: LoxErrors.BadInvite,
    
    34
    -  // failError: LoxErrors.LoxServerUnreachable,
    
    35
    -  // failError: LoxErrors.Other,
    
    33
    +  // failError: LoxError.BadInvite,
    
    34
    +  // failError: LoxError.LoxServerUnreachable,
    
    35
    +  // failError: LoxError.Other,
    
    36 36
       redeemInvite(invite) {
    
    37 37
         return new Promise((res, rej) => {
    
    38 38
           setTimeout(() => {
    
    ... ... @@ -281,13 +281,13 @@ const gProvideBridgeDialog = {
    281 281
               },
    
    282 282
               loxError => {
    
    283 283
                 console.error("Redeeming failed", loxError);
    
    284
    -            switch (loxError.type) {
    
    285
    -              case LoxErrors.BadInvite:
    
    284
    +            switch (loxError instanceof LoxError ? loxError.code : null) {
    
    285
    +              case LoxError.BadInvite:
    
    286 286
                     // TODO: distinguish between a bad invite, an invite that has
    
    287 287
                     // expired, and an invite that has already been redeemed.
    
    288 288
                     this.updateError({ type: "bad-invite" });
    
    289 289
                     break;
    
    290
    -              case LoxErrors.LoxServerUnreachable:
    
    290
    +              case LoxError.LoxServerUnreachable:
    
    291 291
                     this.updateError({ type: "no-server" });
    
    292 292
                     break;
    
    293 293
                   default:
    

  • toolkit/components/lox/Lox.sys.mjs
    ... ... @@ -5,15 +5,32 @@ import {
    5 5
     } from "resource://gre/modules/Timer.sys.mjs";
    
    6 6
     
    
    7 7
     const lazy = {};
    
    8
    +
    
    9
    +ChromeUtils.defineLazyGetter(lazy, "logger", () => {
    
    10
    +  let { ConsoleAPI } = ChromeUtils.importESModule(
    
    11
    +    "resource://gre/modules/Console.sys.mjs"
    
    12
    +  );
    
    13
    +  return new ConsoleAPI({
    
    14
    +    maxLogLevel: "warn",
    
    15
    +    maxLogLevelPref: "lox.log_level",
    
    16
    +    prefix: "Lox",
    
    17
    +  });
    
    18
    +});
    
    19
    +
    
    8 20
     ChromeUtils.defineESModuleGetters(lazy, {
    
    9 21
       DomainFrontRequestBuilder:
    
    10 22
         "resource://gre/modules/DomainFrontedRequests.sys.mjs",
    
    23
    +  DomainFrontRequestNetworkError:
    
    24
    +    "resource://gre/modules/DomainFrontedRequests.sys.mjs",
    
    25
    +  DomainFrontRequestResponseError:
    
    26
    +    "resource://gre/modules/DomainFrontedRequests.sys.mjs",
    
    11 27
       TorConnect: "resource://gre/modules/TorConnect.sys.mjs",
    
    12 28
       TorConnectState: "resource://gre/modules/TorConnect.sys.mjs",
    
    13 29
       TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
    
    14 30
       TorSettingsTopics: "resource://gre/modules/TorSettings.sys.mjs",
    
    15 31
       TorBridgeSource: "resource://gre/modules/TorSettings.sys.mjs",
    
    16 32
     });
    
    33
    +
    
    17 34
     XPCOMUtils.defineLazyModuleGetters(lazy, {
    
    18 35
       init: "resource://gre/modules/lox_wasm.jsm",
    
    19 36
       open_invite: "resource://gre/modules/lox_wasm.jsm",
    
    ... ... @@ -37,13 +54,22 @@ XPCOMUtils.defineLazyModuleGetters(lazy, {
    37 54
       handle_blockage_migration: "resource://gre/modules/lox_wasm.jsm",
    
    38 55
     });
    
    39 56
     
    
    40
    -export const LoxErrors = Object.freeze({
    
    41
    -  BadInvite: "BadInvite",
    
    42
    -  MissingCredential: "MissingCredential",
    
    43
    -  LoxServerUnreachable: "LoxServerUnreachable",
    
    44
    -  NoInvitations: "NoInvitations",
    
    45
    -  InitError: "InitializationError",
    
    46
    -  NotInitialized: "NotInitialized",
    
    57
    +export const LoxTopics = Object.freeze({
    
    58
    +  // Whenever the bridges *might* have changed.
    
    59
    +  // getBridges only uses #credentials, so this will only fire when it changes.
    
    60
    +  UpdateBridges: "lox:update-bridges",
    
    61
    +  // Whenever we gain a new upgrade or blockage event, or clear events.
    
    62
    +  UpdateEvents: "lox:update-events",
    
    63
    +  // Whenever the next unlock *might* have changed.
    
    64
    +  // getNextUnlock uses #credentials and #constants, sow ill fire when either
    
    65
    +  // value changes.
    
    66
    +  UpdateNextUnlock: "lox:update-next-unlock",
    
    67
    +  // Whenever the remaining invites *might* have changed.
    
    68
    +  // getRemainingInviteCount only uses #credentials, so will only fire when it
    
    69
    +  // changes.
    
    70
    +  UpdateRemainingInvites: "lox:update-remaining-invites",
    
    71
    +  // Whenever we generate a new invite.
    
    72
    +  NewInvite: "lox:new-invite",
    
    47 73
     });
    
    48 74
     
    
    49 75
     const LoxSettingsPrefs = Object.freeze({
    
    ... ... @@ -56,10 +82,21 @@ const LoxSettingsPrefs = Object.freeze({
    56 82
       constants: "lox.settings.constants",
    
    57 83
     });
    
    58 84
     
    
    59
    -class LoxError extends Error {
    
    60
    -  constructor(type) {
    
    61
    -    super("");
    
    62
    -    this.type = type;
    
    85
    +/**
    
    86
    + * Error class for Lox.
    
    87
    + */
    
    88
    +export class LoxError extends Error {
    
    89
    +  static BadInvite = "BadInvite";
    
    90
    +  static LoxServerUnreachable = "LoxServerUnreachable";
    
    91
    +
    
    92
    +  /**
    
    93
    +   * @param {string} message - The error message.
    
    94
    +   * @param {string?} [code] - The specific error type, if any.
    
    95
    +   */
    
    96
    +  constructor(message, code = null) {
    
    97
    +    super(message);
    
    98
    +    this.name = "LoxError";
    
    99
    +    this.code = code;
    
    63 100
       }
    
    64 101
     }
    
    65 102
     
    
    ... ... @@ -70,14 +107,65 @@ class LoxImpl {
    70 107
       #encTablePromise = null;
    
    71 108
       #constantsPromise = null;
    
    72 109
       #domainFrontedRequests = null;
    
    73
    -  #invites = null;
    
    110
    +  /**
    
    111
    +   * The list of invites generated.
    
    112
    +   *
    
    113
    +   * @type {string[]}
    
    114
    +   */
    
    115
    +  #invites = [];
    
    74 116
       #pubKeys = null;
    
    75 117
       #encTable = null;
    
    76 118
       #constants = null;
    
    77
    -  #credentials = null;
    
    119
    +  /**
    
    120
    +   * The latest credentials for a given lox id.
    
    121
    +   *
    
    122
    +   * @type {Object<string, string>}
    
    123
    +   */
    
    124
    +  #credentials = {};
    
    125
    +  /**
    
    126
    +   * The list of accumulated blockage or upgrade events.
    
    127
    +   *
    
    128
    +   * This can be cleared when the user acknowledges the events.
    
    129
    +   *
    
    130
    +   * @type {EventData[]}
    
    131
    +   */
    
    78 132
       #events = [];
    
    79 133
       #backgroundInterval = null;
    
    80 134
     
    
    135
    +  /**
    
    136
    +   * The lox ID that is currently active.
    
    137
    +   *
    
    138
    +   * Stays in sync with TorSettings.bridges.lox_id. null when uninitialized.
    
    139
    +   *
    
    140
    +   * @type {string?}
    
    141
    +   */
    
    142
    +  #activeLoxId = null;
    
    143
    +
    
    144
    +  /**
    
    145
    +   * Update the active lox id.
    
    146
    +   */
    
    147
    +  #updateActiveLoxId() {
    
    148
    +    const loxId = lazy.TorSettings.bridges.lox_id;
    
    149
    +    if (loxId === this.#activeLoxId) {
    
    150
    +      return;
    
    151
    +    }
    
    152
    +    lazy.logger.debug(
    
    153
    +      `#activeLoxId switching from "${this.#activeLoxId}" to "${loxId}"`
    
    154
    +    );
    
    155
    +    if (this.#activeLoxId !== null) {
    
    156
    +      lazy.logger.debug(
    
    157
    +        `Clearing event data and invites for "${this.#activeLoxId}"`
    
    158
    +      );
    
    159
    +      // If not initializing clear the metadata for the old lox ID when it
    
    160
    +      // changes.
    
    161
    +      this.clearEventData(this.#activeLoxId);
    
    162
    +      // TODO: Do we want to keep invites? See tor-browser#42453
    
    163
    +      this.#invites = [];
    
    164
    +      this.#store();
    
    165
    +    }
    
    166
    +    this.#activeLoxId = loxId;
    
    167
    +  }
    
    168
    +
    
    81 169
       observe(subject, topic, data) {
    
    82 170
         switch (topic) {
    
    83 171
           case lazy.TorSettingsTopics.SettingsChanged:
    
    ... ... @@ -87,11 +175,8 @@ class LoxImpl {
    87 175
               changes.includes("bridges.source") ||
    
    88 176
               changes.includes("bridges.lox_id")
    
    89 177
             ) {
    
    90
    -          // if lox_id has changed, clear event and invite queues
    
    91
    -          if (changes.includes("bridges.lox_id")) {
    
    92
    -            this.clearEventData();
    
    93
    -            this.clearInvites();
    
    94
    -          }
    
    178
    +          // The lox_id may have changed.
    
    179
    +          this.#updateActiveLoxId();
    
    95 180
     
    
    96 181
               // Only run background tasks if Lox is enabled
    
    97 182
               if (this.#inuse) {
    
    ... ... @@ -108,6 +193,8 @@ class LoxImpl {
    108 193
             }
    
    109 194
             break;
    
    110 195
           case lazy.TorSettingsTopics.Ready:
    
    196
    +        // Set the initial #activeLoxId.
    
    197
    +        this.#updateActiveLoxId();
    
    111 198
             // Run background tasks every 12 hours if Lox is enabled
    
    112 199
             if (this.#inuse) {
    
    113 200
               this.#backgroundInterval = setInterval(
    
    ... ... @@ -119,36 +206,86 @@ class LoxImpl {
    119 206
         }
    
    120 207
       }
    
    121 208
     
    
    209
    +  /**
    
    210
    +   * Assert that the module is initialized.
    
    211
    +   */
    
    212
    +  #assertInitialized() {
    
    213
    +    if (!this.#initialized) {
    
    214
    +      throw new LoxError("Not initialized");
    
    215
    +    }
    
    216
    +  }
    
    217
    +
    
    122 218
       get #inuse() {
    
    123 219
         return (
    
    220
    +      Boolean(this.#activeLoxId) &&
    
    124 221
           lazy.TorSettings.bridges.enabled === true &&
    
    125
    -      lazy.TorSettings.bridges.source === lazy.TorBridgeSource.Lox &&
    
    126
    -      lazy.TorSettings.bridges.lox_id
    
    222
    +      lazy.TorSettings.bridges.source === lazy.TorBridgeSource.Lox
    
    127 223
         );
    
    128 224
       }
    
    129 225
     
    
    226
    +  /**
    
    227
    +   * Change some existing credentials for an ID to a new value.
    
    228
    +   *
    
    229
    +   * @param {string} loxId - The ID to change the credentials for.
    
    230
    +   * @param {string} newCredentials - The new credentials to set.
    
    231
    +   */
    
    232
    +  #changeCredentials(loxId, newCredentials) {
    
    233
    +    // FIXME: Several async methods want to update the credentials, but they
    
    234
    +    // might race and conflict with each. tor-browser#42492
    
    235
    +    if (!newCredentials) {
    
    236
    +      // Avoid overwriting and losing our current credentials.
    
    237
    +      throw new LoxError(`Empty credentials being set for ${loxId}`);
    
    238
    +    }
    
    239
    +    if (!this.#credentials[loxId]) {
    
    240
    +      // Unexpected, but we still want to save the value to storage.
    
    241
    +      lazy.logger.warn(`Lox ID ${loxId} is missing existing credentials`);
    
    242
    +    }
    
    243
    +
    
    244
    +    this.#credentials[loxId] = newCredentials;
    
    245
    +    this.#store();
    
    246
    +
    
    247
    +    // NOTE: In principle we could determine within this module whether the
    
    248
    +    // bridges, remaining invites, or next unlock changes in value when
    
    249
    +    // switching credentials.
    
    250
    +    // However, this logic can be done by the topic observers, as needed. In
    
    251
    +    // particular, TorSettings.bridges.bridge_strings has its own logic
    
    252
    +    // determining whether its value has changed.
    
    253
    +
    
    254
    +    // Let TorSettings know about possibly new bridges.
    
    255
    +    Services.obs.notifyObservers(null, LoxTopics.UpdateBridges);
    
    256
    +    // Let UI know about changes.
    
    257
    +    Services.obs.notifyObservers(null, LoxTopics.UpdateRemainingInvites);
    
    258
    +    Services.obs.notifyObservers(null, LoxTopics.UpdateNextUnlock);
    
    259
    +  }
    
    260
    +
    
    261
    +  /**
    
    262
    +   * Fetch the latest credentials.
    
    263
    +   *
    
    264
    +   * @param {string} loxId - The ID to get the credentials for.
    
    265
    +   *
    
    266
    +   * @returns {string} - The credentials.
    
    267
    +   */
    
    268
    +  #getCredentials(loxId) {
    
    269
    +    const cred = loxId ? this.#credentials[loxId] : undefined;
    
    270
    +    if (!cred) {
    
    271
    +      throw new LoxError(`No credentials for ${loxId}`);
    
    272
    +    }
    
    273
    +    return cred;
    
    274
    +  }
    
    275
    +
    
    130 276
       /**
    
    131 277
        * Formats and returns bridges from the stored Lox credential.
    
    132 278
        *
    
    133
    -   * @param {string} loxid The id string associated with a lox credential.
    
    279
    +   * @param {string} loxId The id string associated with a lox credential.
    
    134 280
        *
    
    135 281
        * @returns {string[]} An array of formatted bridge lines. The array is empty
    
    136 282
        *   if there are no bridges.
    
    137 283
        */
    
    138
    -  getBridges(loxid) {
    
    139
    -    if (!this.#initialized) {
    
    140
    -      throw new LoxError(LoxErrors.NotInitialized);
    
    141
    -    }
    
    142
    -    if (loxid === null) {
    
    143
    -      return [];
    
    144
    -    }
    
    145
    -    if (!this.#credentials[loxid]) {
    
    146
    -      // This lox id doesn't correspond to a stored credential
    
    147
    -      throw new LoxError(LoxErrors.MissingCredential);
    
    148
    -    }
    
    284
    +  getBridges(loxId) {
    
    285
    +    this.#assertInitialized();
    
    149 286
         // Note: this is messy now but can be mostly removed after we have
    
    150 287
         // https://gitlab.torproject.org/tpo/anti-censorship/lox/-/issues/46
    
    151
    -    let bridgelines = JSON.parse(this.#credentials[loxid]).bridgelines;
    
    288
    +    let bridgelines = JSON.parse(this.#getCredentials(loxId)).bridgelines;
    
    152 289
         let bridges = [];
    
    153 290
         for (const bridge of bridgelines) {
    
    154 291
           let addr = bridge.addr;
    
    ... ... @@ -219,18 +356,12 @@ class LoxImpl {
    219 356
       }
    
    220 357
     
    
    221 358
       #load() {
    
    222
    -    if (this.#credentials === null) {
    
    223
    -      let cred = Services.prefs.getStringPref(LoxSettingsPrefs.credentials, "");
    
    224
    -      this.#credentials = cred !== "" ? JSON.parse(cred) : {};
    
    225
    -      let invites = Services.prefs.getStringPref(LoxSettingsPrefs.invites, "");
    
    226
    -      if (invites !== "") {
    
    227
    -        this.#invites = JSON.parse(invites);
    
    228
    -      }
    
    229
    -      let events = Services.prefs.getStringPref(LoxSettingsPrefs.events, "");
    
    230
    -      if (events !== "") {
    
    231
    -        this.#events = JSON.parse(events);
    
    232
    -      }
    
    233
    -    }
    
    359
    +    const cred = Services.prefs.getStringPref(LoxSettingsPrefs.credentials, "");
    
    360
    +    this.#credentials = cred ? JSON.parse(cred) : {};
    
    361
    +    const invites = Services.prefs.getStringPref(LoxSettingsPrefs.invites, "");
    
    362
    +    this.#invites = invites ? JSON.parse(invites) : [];
    
    363
    +    const events = Services.prefs.getStringPref(LoxSettingsPrefs.events, "");
    
    364
    +    this.#events = events ? JSON.parse(events) : [];
    
    234 365
         this.#pubKeys = Services.prefs.getStringPref(
    
    235 366
           LoxSettingsPrefs.pubkeys,
    
    236 367
           null
    
    ... ... @@ -246,16 +377,21 @@ class LoxImpl {
    246 377
       }
    
    247 378
     
    
    248 379
       async #getPubKeys() {
    
    380
    +    // FIXME: We are always refetching #pubKeys, #encTable and #constants once
    
    381
    +    // per session, but they may change more frequently. tor-browser#42502
    
    249 382
         if (this.#pubKeyPromise === null) {
    
    250 383
           this.#pubKeyPromise = this.#makeRequest("pubkeys", [])
    
    251 384
             .then(pubKeys => {
    
    252 385
               this.#pubKeys = JSON.stringify(pubKeys);
    
    253 386
               this.#store();
    
    254 387
             })
    
    255
    -        .catch(() => {
    
    388
    +        .catch(error => {
    
    389
    +          lazy.logger.debug("Failed to get pubkeys", error);
    
    390
    +          // Make the next call try again.
    
    391
    +          this.#pubKeyPromise = null;
    
    256 392
               // We always try to update, but if that doesn't work fall back to stored data
    
    257 393
               if (!this.#pubKeys) {
    
    258
    -            throw new LoxError(LoxErrors.LoxServerUnreachable);
    
    394
    +            throw error;
    
    259 395
               }
    
    260 396
             });
    
    261 397
         }
    
    ... ... @@ -269,10 +405,13 @@ class LoxImpl {
    269 405
               this.#encTable = JSON.stringify(encTable);
    
    270 406
               this.#store();
    
    271 407
             })
    
    272
    -        .catch(() => {
    
    408
    +        .catch(error => {
    
    409
    +          lazy.logger.debug("Failed to get encTable", error);
    
    410
    +          // Make the next call try again.
    
    411
    +          this.#encTablePromise = null;
    
    273 412
               // Try to update first, but if that doesn't work fall back to stored data
    
    274 413
               if (!this.#encTable) {
    
    275
    -            throw new LoxError(LoxErrors.LoxServerUnreachable);
    
    414
    +            throw error;
    
    276 415
               }
    
    277 416
             });
    
    278 417
         }
    
    ... ... @@ -284,55 +423,96 @@ class LoxImpl {
    284 423
           // Try to update first, but if that doesn't work fall back to stored data
    
    285 424
           this.#constantsPromise = this.#makeRequest("constants", [])
    
    286 425
             .then(constants => {
    
    426
    +          const prevValue = this.#constants;
    
    287 427
               this.#constants = JSON.stringify(constants);
    
    288 428
               this.#store();
    
    429
    +          if (prevValue !== this.#constants) {
    
    430
    +            Services.obs.notifyObservers(null, LoxTopics.UpdateNextUnlock);
    
    431
    +          }
    
    289 432
             })
    
    290
    -        .catch(() => {
    
    433
    +        .catch(error => {
    
    434
    +          lazy.logger.debug("Failed to get constants", error);
    
    435
    +          // Make the next call try again.
    
    436
    +          this.#constantsPromise = null;
    
    291 437
               if (!this.#constants) {
    
    292
    -            throw new LoxError(LoxErrors.LoxServerUnreachable);
    
    438
    +            throw error;
    
    293 439
               }
    
    294 440
             });
    
    295 441
         }
    
    296 442
         await this.#constantsPromise;
    
    297 443
       }
    
    298 444
     
    
    445
    +  /**
    
    446
    +   * Parse a decimal string to a non-negative integer.
    
    447
    +   *
    
    448
    +   * @param {string} str - The string to parse.
    
    449
    +   * @returns {integer} - The integer.
    
    450
    +   */
    
    451
    +  static #parseNonNegativeInteger(str) {
    
    452
    +    if (typeof str !== "string" || !/^[0-9]+$/.test(str)) {
    
    453
    +      throw new LoxError(`Expected a non-negative decimal integer: "${str}"`);
    
    454
    +    }
    
    455
    +    return parseInt(str, 10);
    
    456
    +  }
    
    457
    +
    
    458
    +  /**
    
    459
    +   * Get the current lox trust level.
    
    460
    +   *
    
    461
    +   * @param {string} loxId - The ID to fetch the level for.
    
    462
    +   * @returns {integer} - The trust level.
    
    463
    +   */
    
    464
    +  #getLevel(loxId) {
    
    465
    +    return LoxImpl.#parseNonNegativeInteger(
    
    466
    +      lazy.get_trust_level(this.#getCredentials(loxId))
    
    467
    +    );
    
    468
    +  }
    
    469
    +
    
    299 470
       /**
    
    300 471
        * Check for blockages and attempt to perform a levelup
    
    301 472
        *
    
    302 473
        * If either blockages or a levelup happened, add an event to the event queue
    
    303 474
        */
    
    304 475
       async #backgroundTasks() {
    
    305
    -    if (!this.#initialized) {
    
    306
    -      throw new LoxError(LoxErrors.NotInitialized);
    
    476
    +    this.#assertInitialized();
    
    477
    +    let addedEvent = false;
    
    478
    +    // Only run background tasks for the active lox ID.
    
    479
    +    const loxId = this.#activeLoxId;
    
    480
    +    if (!loxId) {
    
    481
    +      lazy.logger.warn("No loxId for the background task");
    
    482
    +      return;
    
    307 483
         }
    
    308
    -    const loxid = lazy.TorSettings.bridges.lox_id;
    
    309 484
         try {
    
    310
    -      const levelup = await this.#attemptUpgrade(loxid);
    
    485
    +      const levelup = await this.#attemptUpgrade(loxId);
    
    311 486
           if (levelup) {
    
    312
    -        const level = lazy.get_trust_level(this.#credentials[loxid]);
    
    487
    +        const level = this.#getLevel(loxId);
    
    313 488
             const newEvent = {
    
    314 489
               type: "levelup",
    
    315 490
               newlevel: level,
    
    316 491
             };
    
    317 492
             this.#events.push(newEvent);
    
    318 493
             this.#store();
    
    494
    +        addedEvent = true;
    
    319 495
           }
    
    320 496
         } catch (err) {
    
    321
    -      console.log(err);
    
    497
    +      lazy.logger.error(err);
    
    322 498
         }
    
    323 499
         try {
    
    324
    -      const leveldown = await this.#blockageMigration(loxid);
    
    500
    +      const leveldown = await this.#blockageMigration(loxId);
    
    325 501
           if (leveldown) {
    
    326
    -        let level = lazy.get_trust_level(this.#credentials[loxid]);
    
    502
    +        let level = this.#getLevel(loxId);
    
    327 503
             const newEvent = {
    
    328 504
               type: "blockage",
    
    329 505
               newlevel: level,
    
    330 506
             };
    
    331 507
             this.#events.push(newEvent);
    
    332 508
             this.#store();
    
    509
    +        addedEvent = true;
    
    333 510
           }
    
    334 511
         } catch (err) {
    
    335
    -      console.log(err);
    
    512
    +      lazy.logger.error(err);
    
    513
    +    }
    
    514
    +    if (addedEvent) {
    
    515
    +      Services.obs.notifyObservers(null, LoxTopics.UpdateEvents);
    
    336 516
         }
    
    337 517
       }
    
    338 518
     
    
    ... ... @@ -356,10 +536,8 @@ class LoxImpl {
    356 536
         await lazy.init(this.#window);
    
    357 537
         lazy.set_panic_hook();
    
    358 538
         if (typeof lazy.open_invite !== "function") {
    
    359
    -      throw new LoxError(LoxErrors.InitError);
    
    539
    +      throw new LoxError("Initialization failed");
    
    360 540
         }
    
    361
    -    this.#invites = [];
    
    362
    -    this.#events = [];
    
    363 541
         this.#load();
    
    364 542
         this.#initialized = true;
    
    365 543
       }
    
    ... ... @@ -376,14 +554,14 @@ class LoxImpl {
    376 554
         }
    
    377 555
         this.#initialized = false;
    
    378 556
         this.#window = null;
    
    379
    -    this.#invites = null;
    
    557
    +    this.#invites = [];
    
    380 558
         this.#pubKeys = null;
    
    381 559
         this.#encTable = null;
    
    382 560
         this.#constants = null;
    
    383 561
         this.#pubKeyPromise = null;
    
    384 562
         this.#encTablePromise = null;
    
    385 563
         this.#constantsPromise = null;
    
    386
    -    this.#credentials = null;
    
    564
    +    this.#credentials = {};
    
    387 565
         this.#events = [];
    
    388 566
         if (this.#backgroundInterval) {
    
    389 567
           clearInterval(this.#backgroundInterval);
    
    ... ... @@ -398,13 +576,11 @@ class LoxImpl {
    398 576
        * @returns {bool} Whether the value passed in was a Lox invitation.
    
    399 577
        */
    
    400 578
       validateInvitation(invite) {
    
    401
    -    if (!this.#initialized) {
    
    402
    -      throw new LoxError(LoxErrors.NotInitialized);
    
    403
    -    }
    
    579
    +    this.#assertInitialized();
    
    404 580
         try {
    
    405 581
           lazy.invitation_is_trusted(invite);
    
    406 582
         } catch (err) {
    
    407
    -      console.log(err);
    
    583
    +      lazy.logger.error(err);
    
    408 584
           return false;
    
    409 585
         }
    
    410 586
         return true;
    
    ... ... @@ -413,11 +589,9 @@ class LoxImpl {
    413 589
       // Note: This is only here for testing purposes. We're going to be using telegram
    
    414 590
       // to issue open invitations for Lox bridges.
    
    415 591
       async requestOpenInvite() {
    
    416
    -    if (!this.#initialized) {
    
    417
    -      throw new LoxError(LoxErrors.NotInitialized);
    
    418
    -    }
    
    592
    +    this.#assertInitialized();
    
    419 593
         let invite = await this.#makeRequest("invite", []);
    
    420
    -    console.log(invite);
    
    594
    +    lazy.logger.debug(invite);
    
    421 595
         return invite;
    
    422 596
       }
    
    423 597
     
    
    ... ... @@ -425,36 +599,37 @@ class LoxImpl {
    425 599
        * Redeems a Lox invitation to obtain a credential and bridges.
    
    426 600
        *
    
    427 601
        * @param {string} invite A Lox invitation.
    
    428
    -   * @returns {string} The loxid of the associated credential on success.
    
    602
    +   * @returns {string} The loxId of the associated credential on success.
    
    429 603
        */
    
    430 604
       async redeemInvite(invite) {
    
    431
    -    if (!this.#initialized) {
    
    432
    -      throw new LoxError(LoxErrors.NotInitialized);
    
    433
    -    }
    
    605
    +    this.#assertInitialized();
    
    434 606
         await this.#getPubKeys();
    
    435 607
         let request = await lazy.open_invite(JSON.parse(invite).invite);
    
    436
    -    let id = this.#genLoxId();
    
    437
    -    let response;
    
    438
    -    try {
    
    439
    -      response = await this.#makeRequest(
    
    440
    -        "openreq",
    
    441
    -        JSON.parse(request).request
    
    442
    -      );
    
    443
    -    } catch {
    
    444
    -      throw new LoxError(LoxErrors.LoxServerUnreachable);
    
    445
    -    }
    
    446
    -    console.log("openreq response: ", response);
    
    608
    +    let response = await this.#makeRequest(
    
    609
    +      "openreq",
    
    610
    +      JSON.parse(request).request
    
    611
    +    );
    
    612
    +    lazy.logger.debug("openreq response: ", response);
    
    447 613
         if (response.hasOwnProperty("error")) {
    
    448
    -      throw new LoxError(LoxErrors.BadInvite);
    
    614
    +      throw new LoxError(
    
    615
    +        `Error response to "openreq": ${response.error}`,
    
    616
    +        LoxError.BadInvite
    
    617
    +      );
    
    449 618
         }
    
    450 619
         let cred = lazy.handle_new_lox_credential(
    
    451 620
           request,
    
    452 621
           JSON.stringify(response),
    
    453 622
           this.#pubKeys
    
    454 623
         );
    
    455
    -    this.#credentials[id] = cred;
    
    624
    +    // Generate an id that is not already in the #credentials map.
    
    625
    +    let loxId;
    
    626
    +    do {
    
    627
    +      loxId = this.#genLoxId();
    
    628
    +    } while (Object.hasOwn(this.#credentials, loxId));
    
    629
    +    // Set new credentials.
    
    630
    +    this.#credentials[loxId] = cred;
    
    456 631
         this.#store();
    
    457
    -    return id;
    
    632
    +    return loxId;
    
    458 633
       }
    
    459 634
     
    
    460 635
       /**
    
    ... ... @@ -463,10 +638,9 @@ class LoxImpl {
    463 638
        * @returns {string[]} A list of all historical invites.
    
    464 639
        */
    
    465 640
       getInvites() {
    
    466
    -    if (!this.#initialized) {
    
    467
    -      throw new LoxError(LoxErrors.NotInitialized);
    
    468
    -    }
    
    469
    -    return this.#invites;
    
    641
    +    this.#assertInitialized();
    
    642
    +    // Return a copy.
    
    643
    +    return structuredClone(this.#invites);
    
    470 644
       }
    
    471 645
     
    
    472 646
       /**
    
    ... ... @@ -477,212 +651,191 @@ class LoxImpl {
    477 651
        *  - there is no saved Lox credential, or
    
    478 652
        *  - the saved credential does not have any invitations available.
    
    479 653
        *
    
    654
    +   * @param {string} loxId - The ID to generate an invite for.
    
    480 655
        * @returns {string} A valid Lox invitation.
    
    481 656
        */
    
    482
    -  async generateInvite() {
    
    483
    -    if (!this.#initialized) {
    
    484
    -      throw new LoxError(LoxErrors.NotInitialized);
    
    485
    -    }
    
    486
    -    const loxid = lazy.TorSettings.bridges.lox_id;
    
    487
    -    if (!loxid || !this.#credentials[loxid]) {
    
    488
    -      throw new LoxError(LoxErrors.MissingCredential);
    
    489
    -    }
    
    657
    +  async generateInvite(loxId) {
    
    658
    +    this.#assertInitialized();
    
    490 659
         await this.#getPubKeys();
    
    491 660
         await this.#getEncTable();
    
    492
    -    let level = lazy.get_trust_level(this.#credentials[loxid]);
    
    661
    +    let level = this.#getLevel(loxId);
    
    493 662
         if (level < 1) {
    
    494
    -      throw new LoxError(LoxErrors.NoInvitations);
    
    663
    +      throw new LoxError(`Cannot generate invites at level ${level}`);
    
    495 664
         }
    
    496 665
         let request = lazy.issue_invite(
    
    497
    -      JSON.stringify(this.#credentials[loxid]),
    
    666
    +      JSON.stringify(this.#getCredentials(loxId)),
    
    498 667
           this.#encTable,
    
    499 668
           this.#pubKeys
    
    500 669
         );
    
    501
    -    let response;
    
    502
    -    try {
    
    503
    -      response = await this.#makeRequest(
    
    504
    -        "issueinvite",
    
    505
    -        JSON.parse(request).request
    
    506
    -      );
    
    507
    -    } catch {
    
    508
    -      throw new LoxError(LoxErrors.LoxServerUnreachable);
    
    509
    -    }
    
    670
    +    let response = await this.#makeRequest(
    
    671
    +      "issueinvite",
    
    672
    +      JSON.parse(request).request
    
    673
    +    );
    
    510 674
         if (response.hasOwnProperty("error")) {
    
    511
    -      console.log(response.error);
    
    512
    -      throw new LoxError(LoxErrors.NoInvitations);
    
    675
    +      lazy.logger.error(response.error);
    
    676
    +      throw new LoxError(`Error response to "issueinvite": ${response.error}`);
    
    513 677
         } else {
    
    514
    -      this.#credentials[loxid] = response;
    
    515 678
           const invite = lazy.prepare_invite(response);
    
    516 679
           this.#invites.push(invite);
    
    517 680
           // cap length of stored invites
    
    518 681
           if (this.#invites.len > 50) {
    
    519 682
             this.#invites.shift();
    
    520 683
           }
    
    521
    -      return invite;
    
    684
    +      this.#store();
    
    685
    +      this.#changeCredentials(loxId, response);
    
    686
    +      Services.obs.notifyObservers(null, LoxTopics.NewInvite);
    
    687
    +      // Return a copy.
    
    688
    +      // Right now invite is just a string, but that might change in the future.
    
    689
    +      return structuredClone(invite);
    
    522 690
         }
    
    523 691
       }
    
    524 692
     
    
    525 693
       /**
    
    526 694
        * Get the number of invites that a user has remaining.
    
    527 695
        *
    
    696
    +   * @param {string} loxId - The ID to check.
    
    528 697
        * @returns {int} The number of invites that can still be generated by a
    
    529 698
        *   user's credential.
    
    530 699
        */
    
    531
    -  getRemainingInviteCount() {
    
    532
    -    if (!this.#initialized) {
    
    533
    -      throw new LoxError(LoxErrors.NotInitialized);
    
    534
    -    }
    
    535
    -    const loxid = lazy.TorSettings.bridges.lox_id;
    
    536
    -    if (!loxid || !this.#credentials[loxid]) {
    
    537
    -      throw new LoxError(LoxErrors.MissingCredential);
    
    538
    -    }
    
    539
    -    return parseInt(lazy.get_invites_remaining(this.#credentials[loxid]));
    
    700
    +  getRemainingInviteCount(loxId) {
    
    701
    +    this.#assertInitialized();
    
    702
    +    return LoxImpl.#parseNonNegativeInteger(
    
    703
    +      lazy.get_invites_remaining(this.#getCredentials(loxId))
    
    704
    +    );
    
    540 705
       }
    
    541 706
     
    
    542
    -  async #blockageMigration(loxid) {
    
    543
    -    if (!loxid || !this.#credentials[loxid]) {
    
    544
    -      throw new LoxError(LoxErrors.MissingCredential);
    
    545
    -    }
    
    707
    +  async #blockageMigration(loxId) {
    
    546 708
         await this.#getPubKeys();
    
    547 709
         let request;
    
    548 710
         try {
    
    549
    -      request = lazy.check_blockage(this.#credentials[loxid], this.#pubKeys);
    
    711
    +      request = lazy.check_blockage(this.#getCredentials(loxId), this.#pubKeys);
    
    550 712
         } catch {
    
    551
    -      console.log("Not ready for blockage migration");
    
    713
    +      lazy.logger.log("Not ready for blockage migration");
    
    552 714
           return false;
    
    553 715
         }
    
    554 716
         let response = await this.#makeRequest("checkblockage", request);
    
    555 717
         if (response.hasOwnProperty("error")) {
    
    556
    -      console.log(response.error);
    
    557
    -      throw new LoxError(LoxErrors.LoxServerUnreachable);
    
    718
    +      lazy.logger.error(response.error);
    
    719
    +      throw new LoxError(
    
    720
    +        `Error response to "checkblockage": ${response.error}`
    
    721
    +      );
    
    558 722
         }
    
    559 723
         const migrationCred = lazy.handle_check_blockage(
    
    560
    -      this.#credentials[loxid],
    
    724
    +      this.#getCredentials(loxId),
    
    561 725
           JSON.stringify(response)
    
    562 726
         );
    
    563 727
         request = lazy.blockage_migration(
    
    564
    -      this.#credentials[loxid],
    
    728
    +      this.#getCredentials(loxId),
    
    565 729
           migrationCred,
    
    566 730
           this.#pubKeys
    
    567 731
         );
    
    568 732
         response = await this.#makeRequest("blockagemigration", request);
    
    569 733
         if (response.hasOwnProperty("error")) {
    
    570
    -      console.log(response.error);
    
    571
    -      throw new LoxError(LoxErrors.LoxServerUnreachable);
    
    734
    +      lazy.logger.error(response.error);
    
    735
    +      throw new LoxError(
    
    736
    +        `Error response to "blockagemigration": ${response.error}`
    
    737
    +      );
    
    572 738
         }
    
    573 739
         const cred = lazy.handle_blockage_migration(
    
    574
    -      this.#credentials[loxid],
    
    740
    +      this.#getCredentials(loxId),
    
    575 741
           JSON.stringify(response),
    
    576 742
           this.#pubKeys
    
    577 743
         );
    
    578
    -    this.#credentials[loxid] = cred;
    
    579
    -    this.#store();
    
    744
    +    this.#changeCredentials(loxId, cred);
    
    580 745
         return true;
    
    581 746
       }
    
    582 747
     
    
    583 748
       /** Attempts to upgrade the currently saved Lox credential.
    
    584 749
        *  If an upgrade is available, save an event in the event list.
    
    585 750
        *
    
    586
    -   *  @returns {boolean} whether a levelup event occured
    
    751
    +   *  @returns {boolean} Whether a levelup event occurred.
    
    587 752
        */
    
    588
    -  async #attemptUpgrade(loxid) {
    
    589
    -    if (!loxid || !this.#credentials[loxid]) {
    
    590
    -      throw new LoxError(LoxErrors.MissingCredential);
    
    591
    -    }
    
    753
    +  async #attemptUpgrade(loxId) {
    
    592 754
         await this.#getPubKeys();
    
    593 755
         await this.#getEncTable();
    
    594 756
         await this.#getConstants();
    
    595
    -    let success = false;
    
    596
    -    let level = lazy.get_trust_level(this.#credentials[loxid]);
    
    757
    +    let level = this.#getLevel(loxId);
    
    597 758
         if (level < 1) {
    
    598 759
           // attempt trust promotion instead
    
    599
    -      try {
    
    600
    -        success = await this.#trustMigration();
    
    601
    -      } catch (err) {
    
    602
    -        console.log(err);
    
    603
    -        return false;
    
    604
    -      }
    
    605
    -    } else {
    
    606
    -      let request = lazy.level_up(
    
    607
    -        this.#credentials[loxid],
    
    608
    -        this.#encTable,
    
    609
    -        this.#pubKeys
    
    610
    -      );
    
    611
    -      const response = await this.#makeRequest("levelup", request);
    
    612
    -      if (response.hasOwnProperty("error")) {
    
    613
    -        console.log(response.error);
    
    614
    -        throw new LoxError(LoxErrors.LoxServerUnreachable);
    
    615
    -      }
    
    616
    -      const cred = lazy.handle_level_up(
    
    617
    -        request,
    
    618
    -        JSON.stringify(response),
    
    619
    -        this.#pubKeys
    
    620
    -      );
    
    621
    -      this.#credentials[loxid] = cred;
    
    622
    -      return true;
    
    760
    +      return this.#trustMigration(loxId);
    
    761
    +    }
    
    762
    +    let request = lazy.level_up(
    
    763
    +      this.#getCredentials(loxId),
    
    764
    +      this.#encTable,
    
    765
    +      this.#pubKeys
    
    766
    +    );
    
    767
    +    const response = await this.#makeRequest("levelup", request);
    
    768
    +    if (response.hasOwnProperty("error")) {
    
    769
    +      lazy.logger.error(response.error);
    
    770
    +      throw new LoxError(`Error response to "levelup": ${response.error}`);
    
    623 771
         }
    
    624
    -    return success;
    
    772
    +    const cred = lazy.handle_level_up(
    
    773
    +      request,
    
    774
    +      JSON.stringify(response),
    
    775
    +      this.#pubKeys
    
    776
    +    );
    
    777
    +    this.#changeCredentials(loxId, cred);
    
    778
    +    return true;
    
    625 779
       }
    
    626 780
     
    
    627 781
       /**
    
    628 782
        * Attempt to migrate from an untrusted to a trusted Lox credential
    
    629 783
        *
    
    630
    -   * @returns {Promise<bool>} A bool value indicated whether the credential
    
    631
    -   *    was successfully migrated.
    
    784
    +   * @param {string} loxId - The ID to use.
    
    785
    +   * @returns {boolean} Whether the credential was successfully migrated.
    
    632 786
        */
    
    633
    -  async #trustMigration() {
    
    634
    -    const loxid = lazy.TorSettings.bridges.lox_id;
    
    635
    -    if (!loxid || !this.#credentials[loxid]) {
    
    636
    -      throw new LoxError(LoxErrors.MissingCredential);
    
    637
    -    }
    
    787
    +  async #trustMigration(loxId) {
    
    638 788
         await this.#getPubKeys();
    
    639 789
         return new Promise((resolve, reject) => {
    
    640 790
           let request = "";
    
    641 791
           try {
    
    642
    -        request = lazy.trust_promotion(this.#credentials[loxid], this.#pubKeys);
    
    792
    +        request = lazy.trust_promotion(
    
    793
    +          this.#getCredentials(loxId),
    
    794
    +          this.#pubKeys
    
    795
    +        );
    
    643 796
           } catch (err) {
    
    644
    -        console.log("Not ready to upgrade");
    
    797
    +        lazy.logger.debug("Not ready to upgrade");
    
    645 798
             resolve(false);
    
    646 799
           }
    
    647 800
           this.#makeRequest("trustpromo", JSON.parse(request).request)
    
    648 801
             .then(response => {
    
    649 802
               if (response.hasOwnProperty("error")) {
    
    803
    +            lazy.logger.error("Error response from trustpromo", response.error);
    
    650 804
                 resolve(false);
    
    651 805
               }
    
    652
    -          console.log("Got promotion cred");
    
    653
    -          console.log(response);
    
    654
    -          console.log(request);
    
    806
    +          lazy.logger.debug("Got promotion cred", response, request);
    
    655 807
               let promoCred = lazy.handle_trust_promotion(
    
    656 808
                 request,
    
    657 809
                 JSON.stringify(response)
    
    658 810
               );
    
    659
    -          console.log("Formatted promotion cred");
    
    811
    +          lazy.logger.debug("Formatted promotion cred");
    
    660 812
               request = lazy.trust_migration(
    
    661
    -            this.#credentials[loxid],
    
    813
    +            this.#getCredentials(loxId),
    
    662 814
                 promoCred,
    
    663 815
                 this.#pubKeys
    
    664 816
               );
    
    665
    -          console.log("Formatted migration request");
    
    817
    +          lazy.logger.debug("Formatted migration request");
    
    666 818
               this.#makeRequest("trustmig", JSON.parse(request).request)
    
    667 819
                 .then(response => {
    
    668 820
                   if (response.hasOwnProperty("error")) {
    
    821
    +                lazy.logger.error(
    
    822
    +                  "Error response from trustmig",
    
    823
    +                  response.error
    
    824
    +                );
    
    669 825
                     resolve(false);
    
    670 826
                   }
    
    671
    -              console.log("Got new credential");
    
    827
    +              lazy.logger.debug("Got new credential");
    
    672 828
                   let cred = lazy.handle_trust_migration(request, response);
    
    673
    -              this.#credentials[loxid] = cred;
    
    674
    -              this.#store();
    
    829
    +              this.#changeCredentials(loxId, cred);
    
    675 830
                   resolve(true);
    
    676 831
                 })
    
    677 832
                 .catch(err => {
    
    678
    -              console.log(err);
    
    679
    -              console.log("Failed trust migration");
    
    833
    +              lazy.logger.error("Failed trust migration", err);
    
    680 834
                   resolve(false);
    
    681 835
                 });
    
    682 836
             })
    
    683 837
             .catch(err => {
    
    684
    -          console.log(err);
    
    685
    -          console.log("Failed trust promotion");
    
    838
    +          lazy.logger.error("Failed trust promotion", err);
    
    686 839
               resolve(false);
    
    687 840
             });
    
    688 841
         });
    
    ... ... @@ -701,88 +854,105 @@ class LoxImpl {
    701 854
       /**
    
    702 855
        * Get a list of accumulated events.
    
    703 856
        *
    
    857
    +   * @param {string} loxId - The ID to get events for.
    
    704 858
        * @returns {EventData[]} A list of the accumulated, unacknowledged events
    
    705 859
        *   associated with a user's credential.
    
    706 860
        */
    
    707
    -  getEventData() {
    
    708
    -    if (!this.#initialized) {
    
    709
    -      throw new LoxError(LoxErrors.NotInitialized);
    
    710
    -    }
    
    711
    -    const loxid = lazy.TorSettings.bridges.lox_id;
    
    712
    -    if (!loxid || !this.#credentials[loxid]) {
    
    713
    -      throw new LoxError(LoxErrors.MissingCredential);
    
    861
    +  getEventData(loxId) {
    
    862
    +    this.#assertInitialized();
    
    863
    +    if (loxId !== this.#activeLoxId) {
    
    864
    +      lazy.logger.warn(
    
    865
    +        `No event data for loxId ${loxId} since it was replaced by ${
    
    866
    +          this.#activeLoxId
    
    867
    +        }`
    
    868
    +      );
    
    869
    +      return [];
    
    714 870
         }
    
    715
    -    return this.#events;
    
    871
    +    // Return a copy.
    
    872
    +    return structuredClone(this.#events);
    
    716 873
       }
    
    717 874
     
    
    718 875
       /**
    
    719 876
        * Clears accumulated event data.
    
    877
    +   *
    
    878
    +   * Should be called whenever the user acknowledges the existing events.
    
    879
    +   *
    
    880
    +   * @param {string} loxId - The ID to clear events for.
    
    720 881
        */
    
    721
    -  clearEventData() {
    
    722
    -    if (!this.#initialized) {
    
    723
    -      throw new LoxError(LoxErrors.NotInitialized);
    
    882
    +  clearEventData(loxId) {
    
    883
    +    this.#assertInitialized();
    
    884
    +    if (loxId !== this.#activeLoxId) {
    
    885
    +      lazy.logger.warn(
    
    886
    +        `Not clearing event data for loxId ${loxId} since it was replaced by ${
    
    887
    +          this.#activeLoxId
    
    888
    +        }`
    
    889
    +      );
    
    890
    +      return;
    
    724 891
         }
    
    725 892
         this.#events = [];
    
    726 893
         this.#store();
    
    727
    -  }
    
    728
    -
    
    729
    -  /**
    
    730
    -   * Clears accumulated invitations.
    
    731
    -   */
    
    732
    -  clearInvites() {
    
    733
    -    if (!this.#initialized) {
    
    734
    -      throw new LoxError(LoxErrors.NotInitialized);
    
    735
    -    }
    
    736
    -    this.#invites = [];
    
    737
    -    this.#store();
    
    894
    +    Services.obs.notifyObservers(null, LoxTopics.UpdateEvents);
    
    738 895
       }
    
    739 896
     
    
    740 897
       /**
    
    741 898
        * @typedef {object} UnlockData
    
    742 899
        *
    
    743
    -   * @property {string} date - The date-time for the next level up, formatted as YYYY-MM-DDTHH:mm:ssZ.
    
    744
    -   * @property {integer} nextLevel - The next level. Levels count from 0, so this will be 1 or greater.
    
    745
    -   *
    
    900
    +   * @property {string} date - The date-time for the next level up, formatted as
    
    901
    +   *   YYYY-MM-DDTHH:mm:ssZ.
    
    902
    +   * @property {integer} nextLevel - The next level. Levels count from 0, so
    
    903
    +   *   this will be 1 or greater.
    
    746 904
        */
    
    747 905
     
    
    748 906
       /**
    
    749 907
        * Get details about the next feature unlock.
    
    750 908
        *
    
    909
    +   * NOTE: A call to this method may trigger LoxTopics.UpdateNextUnlock.
    
    910
    +   *
    
    911
    +   * @param {string} loxId - The ID to get the unlock for.
    
    751 912
        * @returns {UnlockData} - Details about the next unlock.
    
    752 913
        */
    
    753
    -  async getNextUnlock() {
    
    754
    -    if (!this.#initialized) {
    
    755
    -      throw new LoxError(LoxErrors.NotInitialized);
    
    756
    -    }
    
    757
    -    const loxid = lazy.TorSettings.bridges.lox_id;
    
    758
    -    if (!loxid || !this.#credentials[loxid]) {
    
    759
    -      throw new LoxError(LoxErrors.MissingCredential);
    
    760
    -    }
    
    914
    +  async getNextUnlock(loxId) {
    
    915
    +    this.#assertInitialized();
    
    761 916
         await this.#getConstants();
    
    762
    -    let nextUnlocks = JSON.parse(
    
    763
    -      lazy.get_next_unlock(this.#constants, this.#credentials[loxid])
    
    917
    +    let nextUnlock = JSON.parse(
    
    918
    +      lazy.get_next_unlock(this.#constants, this.#getCredentials(loxId))
    
    764 919
         );
    
    765
    -    const level = parseInt(lazy.get_trust_level(this.#credentials[loxid]));
    
    766
    -    const unlocks = {
    
    767
    -      date: nextUnlocks.trust_level_unlock_date,
    
    920
    +    const level = this.#getLevel(loxId);
    
    921
    +    return {
    
    922
    +      date: nextUnlock.trust_level_unlock_date,
    
    768 923
           nextLevel: level + 1,
    
    769 924
         };
    
    770
    -    return unlocks;
    
    771 925
       }
    
    772 926
     
    
    773 927
       async #makeRequest(procedure, args) {
    
    774 928
         // TODO: Customize to for Lox
    
    775
    -    const serviceUrl = "https://rdsys-frontend-01.torproject.org/lox";
    
    929
    +    const serviceUrl = "https://lox.torproject.org";
    
    776 930
         const url = `${serviceUrl}/${procedure}`;
    
    777 931
     
    
    778 932
         if (lazy.TorConnect.state === lazy.TorConnectState.Bootstrapped) {
    
    779
    -      const request = await fetch(url, {
    
    780
    -        method: "POST",
    
    781
    -        headers: {
    
    782
    -          "Content-Type": "application/vnd.api+json",
    
    783
    -        },
    
    784
    -        body: JSON.stringify(args),
    
    785
    -      });
    
    933
    +      let request;
    
    934
    +      try {
    
    935
    +        request = await fetch(url, {
    
    936
    +          method: "POST",
    
    937
    +          headers: {
    
    938
    +            "Content-Type": "application/vnd.api+json",
    
    939
    +          },
    
    940
    +          body: JSON.stringify(args),
    
    941
    +        });
    
    942
    +      } catch (error) {
    
    943
    +        lazy.logger.debug("fetch fail", url, args, error);
    
    944
    +        throw new LoxError(
    
    945
    +          `fetch "${procedure}" from Lox authority failed: ${error?.message}`,
    
    946
    +          LoxError.LoxServerUnreachable
    
    947
    +        );
    
    948
    +      }
    
    949
    +      if (!request.ok) {
    
    950
    +        lazy.logger.debug("fetch response", url, args, request);
    
    951
    +        // Do not treat as a LoxServerUnreachable type.
    
    952
    +        throw new LoxError(
    
    953
    +          `Lox authority responded to "${procedure}" with ${request.status}: ${request.statusText}`
    
    954
    +        );
    
    955
    +      }
    
    786 956
           return request.json();
    
    787 957
         }
    
    788 958
     
    
    ... ... @@ -803,7 +973,26 @@ class LoxImpl {
    803 973
           });
    
    804 974
         }
    
    805 975
         const builder = await this.#domainFrontedRequests;
    
    806
    -    return builder.buildPostRequest(url, args);
    
    976
    +    try {
    
    977
    +      return await builder.buildPostRequest(url, args);
    
    978
    +    } catch (error) {
    
    979
    +      lazy.logger.debug("Domain front request fail", url, args, error);
    
    980
    +      if (error instanceof lazy.DomainFrontRequestNetworkError) {
    
    981
    +        throw new LoxError(
    
    982
    +          `Domain front fetch "${procedure}" from Lox authority failed: ${error?.message}`,
    
    983
    +          LoxError.LoxServerUnreachable
    
    984
    +        );
    
    985
    +      }
    
    986
    +      if (error instanceof lazy.DomainFrontRequestResponseError) {
    
    987
    +        // Do not treat as a LoxServerUnreachable type.
    
    988
    +        throw new LoxError(
    
    989
    +          `Lox authority responded to domain front "${procedure}" with ${error.status}: ${error.statusText}`
    
    990
    +        );
    
    991
    +      }
    
    992
    +      throw new LoxError(
    
    993
    +        `Domain front request for "${procedure}" from Lox authority failed: ${error?.message}`
    
    994
    +      );
    
    995
    +    }
    
    807 996
       }
    
    808 997
     }
    
    809 998
     
    

  • toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs
    ... ... @@ -429,20 +429,6 @@ export const TorLauncherUtil = Object.freeze({
    429 429
         return aStringName;
    
    430 430
       },
    
    431 431
     
    
    432
    -  getLocalizedStringForError(aNSResult) {
    
    433
    -    for (let prop in Cr) {
    
    434
    -      if (Cr[prop] === aNSResult) {
    
    435
    -        const key = "nsresult." + prop;
    
    436
    -        const rv = this.getLocalizedString(key);
    
    437
    -        if (rv !== key) {
    
    438
    -          return rv;
    
    439
    -        }
    
    440
    -        return prop; // As a fallback, return the NS_ERROR... name.
    
    441
    -      }
    
    442
    -    }
    
    443
    -    return undefined;
    
    444
    -  },
    
    445
    -
    
    446 432
       getLocalizedBootstrapStatus(aStatusObj, aKeyword) {
    
    447 433
         if (!aStatusObj || !aKeyword) {
    
    448 434
           return "";
    

  • toolkit/modules/DomainFrontedRequests.sys.mjs
    ... ... @@ -347,6 +347,31 @@ class MeekTransportAndroid {
    347 347
       }
    
    348 348
     }
    
    349 349
     
    
    350
    +/**
    
    351
    + * Corresponds to a Network error with the request.
    
    352
    + */
    
    353
    +export class DomainFrontRequestNetworkError extends Error {
    
    354
    +  constructor(request, statusCode) {
    
    355
    +    super(`Error fetching ${request.name}: ${statusCode}`);
    
    356
    +    this.name = "DomainFrontRequestNetworkError";
    
    357
    +    this.statusCode = statusCode;
    
    358
    +  }
    
    359
    +}
    
    360
    +
    
    361
    +/**
    
    362
    + * Corresponds to a non-ok response from the server.
    
    363
    + */
    
    364
    +export class DomainFrontRequestResponseError extends Error {
    
    365
    +  constructor(request) {
    
    366
    +    super(
    
    367
    +      `Error response from ${request.name} server: ${request.responseStatus}`
    
    368
    +    );
    
    369
    +    this.name = "DomainFrontRequestResponseError";
    
    370
    +    this.status = request.responseStatus;
    
    371
    +    this.statusText = request.responseStatusText;
    
    372
    +  }
    
    373
    +}
    
    374
    +
    
    350 375
     /**
    
    351 376
      * Callback object to promisify the XPCOM request.
    
    352 377
      */
    
    ... ... @@ -379,12 +404,11 @@ class ResponseListener {
    379 404
       onStopRequest(request, status) {
    
    380 405
         try {
    
    381 406
           if (!Components.isSuccessCode(status)) {
    
    382
    -        const errorMessage =
    
    383
    -          lazy.TorLauncherUtil.getLocalizedStringForError(status);
    
    384
    -        this.#reject(new Error(errorMessage));
    
    407
    +        // Assume this is a network error.
    
    408
    +        this.#reject(new DomainFrontRequestNetworkError(request, status));
    
    385 409
           }
    
    386 410
           if (request.responseStatus !== 200) {
    
    387
    -        this.#reject(new Error(request.responseStatusText));
    
    411
    +        this.#reject(new DomainFrontRequestResponseError(request));
    
    388 412
           }
    
    389 413
         } catch (err) {
    
    390 414
           this.#reject(err);
    

  • toolkit/modules/TorSettings.sys.mjs
    ... ... @@ -7,6 +7,7 @@ const lazy = {};
    7 7
     ChromeUtils.defineESModuleGetters(lazy, {
    
    8 8
       TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
    
    9 9
       Lox: "resource://gre/modules/Lox.sys.mjs",
    
    10
    +  LoxTopics: "resource://gre/modules/Lox.sys.mjs",
    
    10 11
       TorParsers: "resource://gre/modules/TorParsers.sys.mjs",
    
    11 12
       TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
    
    12 13
     });
    
    ... ... @@ -330,7 +331,17 @@ class TorSettingsImpl {
    330 331
               if (!val) {
    
    331 332
                 return;
    
    332 333
               }
    
    333
    -          this.bridges.bridge_strings = lazy.Lox.getBridges(val);
    
    334
    +          let bridgeStrings;
    
    335
    +          try {
    
    336
    +            bridgeStrings = lazy.Lox.getBridges(val);
    
    337
    +          } catch (error) {
    
    338
    +            addError(`No bridges for lox_id ${val}: ${error?.message}`);
    
    339
    +            // Set as invalid, which will make the builtin_type "" and set the
    
    340
    +            // bridge_strings to be empty at the next #cleanupSettings.
    
    341
    +            this.bridges.source = TorBridgeSource.Invalid;
    
    342
    +            return;
    
    343
    +          }
    
    344
    +          this.bridges.bridge_strings = bridgeStrings;
    
    334 345
             },
    
    335 346
           },
    
    336 347
         });
    
    ... ... @@ -692,7 +703,7 @@ class TorSettingsImpl {
    692 703
         try {
    
    693 704
           await lazy.Lox.init();
    
    694 705
         } catch (e) {
    
    695
    -      lazy.logger.error("Could not initialize Lox.", e.type);
    
    706
    +      lazy.logger.error("Could not initialize Lox.", e);
    
    696 707
         }
    
    697 708
     
    
    698 709
         if (
    
    ... ... @@ -711,6 +722,8 @@ class TorSettingsImpl {
    711 722
           }
    
    712 723
         }
    
    713 724
     
    
    725
    +    Services.obs.addObserver(this, lazy.LoxTopics.UpdateBridges);
    
    726
    +
    
    714 727
         lazy.logger.info("Ready");
    
    715 728
       }
    
    716 729
     
    
    ... ... @@ -718,9 +731,28 @@ class TorSettingsImpl {
    718 731
        * Unload or uninit our settings.
    
    719 732
        */
    
    720 733
       async uninit() {
    
    734
    +    Services.obs.removeObserver(this, lazy.LoxTopics.UpdateBridges);
    
    721 735
         await lazy.Lox.uninit();
    
    722 736
       }
    
    723 737
     
    
    738
    +  observe(subject, topic, data) {
    
    739
    +    switch (topic) {
    
    740
    +      case lazy.LoxTopics.UpdateBridges:
    
    741
    +        if (this.bridges.lox_id) {
    
    742
    +          // Fetch the newest bridges.
    
    743
    +          this.bridges.bridge_strings = lazy.Lox.getBridges(
    
    744
    +            this.bridges.lox_id
    
    745
    +          );
    
    746
    +          // No need to save to prefs since bridge_strings is not stored for Lox
    
    747
    +          // source. But we do pass on the changes to TorProvider.
    
    748
    +          // FIXME: This can compete with TorConnect to reach TorProvider.
    
    749
    +          // tor-browser#42316
    
    750
    +          this.applySettings();
    
    751
    +        }
    
    752
    +        break;
    
    753
    +    }
    
    754
    +  }
    
    755
    +
    
    724 756
       /**
    
    725 757
        * Check whether the object has been successfully initialized, and throw if
    
    726 758
        * it has not.
    
    ... ... @@ -763,24 +795,32 @@ class TorSettingsImpl {
    763 795
           TorSettingsPrefs.bridges.source,
    
    764 796
           TorBridgeSource.Invalid
    
    765 797
         );
    
    766
    -    this.bridges.lox_id = Services.prefs.getStringPref(
    
    767
    -      TorSettingsPrefs.bridges.lox_id,
    
    768
    -      ""
    
    769
    -    );
    
    770
    -    if (this.bridges.source == TorBridgeSource.BuiltIn) {
    
    771
    -      this.bridges.builtin_type = Services.prefs.getStringPref(
    
    772
    -        TorSettingsPrefs.bridges.builtin_type,
    
    773
    -        ""
    
    774
    -      );
    
    775
    -    } else {
    
    776
    -      const bridgeBranchPrefs = Services.prefs
    
    777
    -        .getBranch(TorSettingsPrefs.bridges.bridge_strings)
    
    778
    -        .getChildList("");
    
    779
    -      this.bridges.bridge_strings = Array.from(bridgeBranchPrefs, pref =>
    
    780
    -        Services.prefs.getStringPref(
    
    781
    -          `${TorSettingsPrefs.bridges.bridge_strings}${pref}`
    
    782
    -        )
    
    783
    -      );
    
    798
    +    switch (this.bridges.source) {
    
    799
    +      case TorBridgeSource.BridgeDB:
    
    800
    +      case TorBridgeSource.UserProvided:
    
    801
    +        this.bridges.bridge_strings = Services.prefs
    
    802
    +          .getBranch(TorSettingsPrefs.bridges.bridge_strings)
    
    803
    +          .getChildList("")
    
    804
    +          .map(pref =>
    
    805
    +            Services.prefs.getStringPref(
    
    806
    +              `${TorSettingsPrefs.bridges.bridge_strings}${pref}`
    
    807
    +            )
    
    808
    +          );
    
    809
    +        break;
    
    810
    +      case TorBridgeSource.BuiltIn:
    
    811
    +        // bridge_strings is set via builtin_type.
    
    812
    +        this.bridges.builtin_type = Services.prefs.getStringPref(
    
    813
    +          TorSettingsPrefs.bridges.builtin_type,
    
    814
    +          ""
    
    815
    +        );
    
    816
    +        break;
    
    817
    +      case TorBridgeSource.Lox:
    
    818
    +        // bridge_strings is set via lox id.
    
    819
    +        this.bridges.lox_id = Services.prefs.getStringPref(
    
    820
    +          TorSettingsPrefs.bridges.lox_id,
    
    821
    +          ""
    
    822
    +        );
    
    823
    +        break;
    
    784 824
         }
    
    785 825
         /* Proxy */
    
    786 826
         this.proxy.enabled = Services.prefs.getBoolPref(
    
    ... ... @@ -866,7 +906,10 @@ class TorSettingsImpl {
    866 906
           );
    
    867 907
         });
    
    868 908
         // write new ones
    
    869
    -    if (this.bridges.source !== TorBridgeSource.BuiltIn) {
    
    909
    +    if (
    
    910
    +      this.bridges.source !== TorBridgeSource.Lox &&
    
    911
    +      this.bridges.source !== TorBridgeSource.BuiltIn
    
    912
    +    ) {
    
    870 913
           this.bridges.bridge_strings.forEach((string, index) => {
    
    871 914
             Services.prefs.setStringPref(
    
    872 915
               `${TorSettingsPrefs.bridges.bridge_strings}.${index}`,
    

  • toolkit/torbutton/chrome/locale/en-US/torlauncher.properties
    ... ... @@ -55,7 +55,3 @@ torlauncher.bootstrapWarning.timeout=connection timeout
    55 55
     torlauncher.bootstrapWarning.noroute=no route to host
    
    56 56
     torlauncher.bootstrapWarning.ioerror=read/write error
    
    57 57
     torlauncher.bootstrapWarning.pt_missing=missing pluggable transport
    58
    -
    
    59
    -torlauncher.nsresult.NS_ERROR_NET_RESET=The connection to the server was lost.
    
    60
    -torlauncher.nsresult.NS_ERROR_CONNECTION_REFUSED=Could not connect to the server.
    
    61
    -torlauncher.nsresult.NS_ERROR_PROXY_CONNECTION_REFUSED=Could not connect to the proxy.