tor-browser

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

NetworkGeolocationProvider.sys.mjs (11222B)


      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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  LocationHelper: "resource://gre/modules/LocationHelper.sys.mjs",
     11  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
     12  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     13 });
     14 
     15 // GeolocationPositionError has no interface object, so we can't use that here.
     16 const POSITION_UNAVAILABLE = 2;
     17 
     18 XPCOMUtils.defineLazyPreferenceGetter(
     19  lazy,
     20  "gLoggingEnabled",
     21  "geo.provider.network.logging.enabled",
     22  false
     23 );
     24 
     25 function LOG(aMsg) {
     26  if (lazy.gLoggingEnabled) {
     27    dump("*** WIFI GEO: " + aMsg + "\n");
     28  }
     29 }
     30 
     31 function CachedRequest(loc, wifiList) {
     32  this.location = loc;
     33 
     34  let wifis = new Set();
     35  if (wifiList) {
     36    for (let i = 0; i < wifiList.length; i++) {
     37      wifis.add(wifiList[i].macAddress);
     38    }
     39  }
     40 
     41  this.hasWifis = () => wifis.size > 0;
     42 
     43  // if 50% of the SSIDS match
     44  this.isWifiApproxEqual = function (wifiList) {
     45    if (!this.hasWifis()) {
     46      return false;
     47    }
     48 
     49    // if either list is a 50% subset of the other, they are equal
     50    let common = 0;
     51    for (let i = 0; i < wifiList.length; i++) {
     52      if (wifis.has(wifiList[i].macAddress)) {
     53        common++;
     54      }
     55    }
     56    let kPercentMatch = 0.5;
     57    return common >= Math.max(wifis.size, wifiList.length) * kPercentMatch;
     58  };
     59 
     60  this.isGeoip = function () {
     61    return !this.hasWifis();
     62  };
     63 }
     64 
     65 /** @type {CachedRequest?} */
     66 var gCachedRequest = null;
     67 var gDebugCacheReasoning = ""; // for logging the caching logic
     68 
     69 // This function serves two purposes:
     70 // 1) do we have a cached request
     71 // 2) is the cached request better than what newWifiList will obtain
     72 // If the cached request exists, and we know it to have greater accuracy
     73 // by the nature of its origin (wifi/geoip), use its cached location.
     74 //
     75 // If there is more source info than the cached request had, return false
     76 // In other cases, MLS is known to produce better/worse accuracy based on the
     77 // inputs, so base the decision on that.
     78 function isCachedRequestMoreAccurateThanServerRequest(newWifiList) {
     79  gDebugCacheReasoning = "";
     80  let isNetworkRequestCacheEnabled = Services.prefs.getBoolPref(
     81    "geo.provider.network.debug.requestCache.enabled",
     82    true
     83  );
     84  // Mochitest needs this pref to simulate request failure
     85  if (!isNetworkRequestCacheEnabled) {
     86    gCachedRequest = null;
     87  }
     88 
     89  if (!gCachedRequest || !isNetworkRequestCacheEnabled) {
     90    gDebugCacheReasoning = "No cached data";
     91    return false;
     92  }
     93 
     94  if (!newWifiList) {
     95    gDebugCacheReasoning = "New req. is GeoIP.";
     96    return true;
     97  }
     98 
     99  let hasEqualWifis = false;
    100  if (newWifiList) {
    101    hasEqualWifis = gCachedRequest.isWifiApproxEqual(newWifiList);
    102  }
    103 
    104  gDebugCacheReasoning = `EqualWifis: ${hasEqualWifis}`;
    105 
    106  if (gCachedRequest.hasWifis() && hasEqualWifis) {
    107    gDebugCacheReasoning += ", Wifi only.";
    108    return true;
    109  }
    110 
    111  return false;
    112 }
    113 
    114 function NetworkGeoCoordsObject(lat, lon, acc) {
    115  this.latitude = lat;
    116  this.longitude = lon;
    117  this.accuracy = acc;
    118 
    119  // Neither GLS nor MLS return the following properties, so set them to NaN
    120  // here. nsGeoPositionCoords will convert NaNs to null for optional properties
    121  // of the JavaScript Coordinates object.
    122  this.altitude = NaN;
    123  this.altitudeAccuracy = NaN;
    124  this.heading = NaN;
    125  this.speed = NaN;
    126 }
    127 
    128 NetworkGeoCoordsObject.prototype = {
    129  QueryInterface: ChromeUtils.generateQI(["nsIDOMGeoPositionCoords"]),
    130 };
    131 
    132 function NetworkGeoPositionObject(lat, lng, acc) {
    133  this.coords = new NetworkGeoCoordsObject(lat, lng, acc);
    134  this.address = null;
    135  this.timestamp = Date.now();
    136 }
    137 
    138 NetworkGeoPositionObject.prototype = {
    139  QueryInterface: ChromeUtils.generateQI(["nsIDOMGeoPosition"]),
    140 };
    141 
    142 export function NetworkGeolocationProvider() {
    143  /*
    144    The _wifiMonitorTimeout controls how long we wait on receiving an update
    145    from the Wifi subsystem.  If this timer fires, we believe the Wifi scan has
    146    had a problem and we no longer can use Wifi to position the user this time
    147    around (we will continue to be hopeful that Wifi will recover).
    148  */
    149  XPCOMUtils.defineLazyPreferenceGetter(
    150    this,
    151    "_wifiMonitorTimeout",
    152    "geo.provider.network.timeToWaitBeforeSending",
    153    5000
    154  );
    155 
    156  XPCOMUtils.defineLazyPreferenceGetter(
    157    this,
    158    "_wifiScanningEnabled",
    159    "geo.provider.network.scan",
    160    true
    161  );
    162 
    163  this.wifiService = null;
    164  this.timer = null;
    165  this.started = false;
    166 }
    167 
    168 NetworkGeolocationProvider.prototype = {
    169  classID: Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"),
    170  name: "NetworkGeolocationProvider",
    171  QueryInterface: ChromeUtils.generateQI([
    172    "nsIGeolocationProvider",
    173    "nsIWifiListener",
    174    "nsITimerCallback",
    175    "nsIObserver",
    176    "nsINamed",
    177  ]),
    178  listener: null,
    179 
    180  get isWifiScanningEnabled() {
    181    return Cc["@mozilla.org/wifi/monitor;1"] && this._wifiScanningEnabled;
    182  },
    183 
    184  resetTimer() {
    185    if (this.timer) {
    186      this.timer.cancel();
    187      this.timer = null;
    188    }
    189    // Wifi thread triggers NetworkGeolocationProvider to proceed. With no wifi,
    190    // do manual timeout.
    191    this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    192    this.timer.initWithCallback(
    193      this,
    194      this._wifiMonitorTimeout,
    195      this.timer.TYPE_REPEATING_SLACK
    196    );
    197  },
    198 
    199  startup() {
    200    LOG("startup called.");
    201    if (this.started) {
    202      return;
    203    }
    204 
    205    this.started = true;
    206 
    207    if (this.isWifiScanningEnabled) {
    208      if (this.wifiService) {
    209        this.wifiService.stopWatching(this);
    210      }
    211      this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(
    212        Ci.nsIWifiMonitor
    213      );
    214      this.wifiService.startWatching(this, false);
    215    }
    216 
    217    this.resetTimer();
    218  },
    219 
    220  watch(c) {
    221    LOG("watch called");
    222    this.listener = c;
    223    this.notify();
    224    this.resetTimer();
    225  },
    226 
    227  shutdown() {
    228    LOG("shutdown called");
    229    if (!this.started) {
    230      return;
    231    }
    232 
    233    // Without clearing this, we could end up using the cache almost indefinitely
    234    // TODO: add logic for cache lifespan, for now just be safe and clear it
    235    gCachedRequest = null;
    236 
    237    if (this.timer) {
    238      this.timer.cancel();
    239      this.timer = null;
    240    }
    241 
    242    if (this.wifiService) {
    243      this.wifiService.stopWatching(this);
    244      this.wifiService = null;
    245    }
    246 
    247    this.listener = null;
    248    this.started = false;
    249  },
    250 
    251  setHighAccuracy(enable) {
    252    // Mochitest wants to check this value
    253    if (Services.prefs.getBoolPref("geo.provider.testing", false)) {
    254      Services.obs.notifyObservers(
    255        null,
    256        "testing-geolocation-high-accuracy",
    257        enable
    258      );
    259    }
    260  },
    261 
    262  onChange(accessPoints) {
    263    // we got some wifi data, rearm the timer.
    264    this.resetTimer();
    265 
    266    let wifiData = null;
    267    if (accessPoints) {
    268      wifiData = lazy.LocationHelper.formatWifiAccessPoints(accessPoints);
    269    }
    270    this.sendLocationRequest(wifiData);
    271  },
    272 
    273  onError(code) {
    274    LOG("wifi error: " + code);
    275    this.sendLocationRequest(null);
    276  },
    277 
    278  onStatus(err, statusMessage) {
    279    if (!this.listener) {
    280      return;
    281    }
    282    LOG("onStatus called." + statusMessage);
    283 
    284    if (statusMessage && this.listener.notifyStatus) {
    285      this.listener.notifyStatus(statusMessage);
    286    }
    287 
    288    if (err && this.listener.notifyError) {
    289      this.listener.notifyError(POSITION_UNAVAILABLE, statusMessage);
    290    }
    291  },
    292 
    293  notify() {
    294    this.onStatus(false, "wifi-timeout");
    295    this.sendLocationRequest(null);
    296  },
    297 
    298  /**
    299   * After wifi data has been gathered, this method is invoked to perform the
    300   * request to network geolocation provider.
    301   * The result of each request is sent to all registered listener (@see watch)
    302   * by invoking its respective `update`, `notifyError` or `notifyStatus`
    303   * callbacks.
    304   * `update` is called upon a successful request with its response data; this will be a `NetworkGeoPositionObject` instance.
    305   * `notifyError` is called whenever the request gets an error from the local
    306   * network subsystem, the server or simply times out.
    307   * `notifyStatus` is called for each status change of the request that may be
    308   * of interest to the consumer of this class. Currently the following status
    309   * changes are reported: 'xhr-start', 'xhr-timeout', 'xhr-error' and
    310   * 'xhr-empty'.
    311   *
    312   * @param  {Array} wifiData Optional set of publicly available wifi networks
    313   *                          in the following structure:
    314   *                          <code>
    315   *                          [
    316   *                            { macAddress: <mac1>, signalStrength: <signal1> },
    317   *                            { macAddress: <mac2>, signalStrength: <signal2> }
    318   *                          ]
    319   *                          </code>
    320   */
    321  async sendLocationRequest(wifiData) {
    322    let data = { wifiAccessPoints: undefined };
    323    if (wifiData && wifiData.length >= 2) {
    324      data.wifiAccessPoints = wifiData;
    325    }
    326 
    327    let useCached = isCachedRequestMoreAccurateThanServerRequest(
    328      data.wifiAccessPoints
    329    );
    330 
    331    LOG("Use request cache:" + useCached + " reason:" + gDebugCacheReasoning);
    332 
    333    if (useCached) {
    334      gCachedRequest.location.timestamp = Date.now();
    335      if (this.listener) {
    336        this.listener.update(gCachedRequest.location);
    337      }
    338      return;
    339    }
    340 
    341    // From here on, do a network geolocation request //
    342    let url = Services.urlFormatter.formatURLPref("geo.provider.network.url");
    343    LOG("Sending request");
    344 
    345    let result;
    346    try {
    347      result = await this.makeRequest(url, wifiData);
    348      LOG(
    349        `geo provider reported: ${result.location.lng}:${result.location.lat}`
    350      );
    351      let newLocation = new NetworkGeoPositionObject(
    352        result.location.lat,
    353        result.location.lng,
    354        result.accuracy
    355      );
    356 
    357      if (this.listener) {
    358        this.listener.update(newLocation);
    359      }
    360 
    361      gCachedRequest = new CachedRequest(newLocation, data.wifiAccessPoints);
    362    } catch (err) {
    363      LOG("Location request hit error: " + err.name);
    364      console.error(err);
    365      if (err.name == "AbortError") {
    366        this.onStatus(true, "xhr-timeout");
    367      } else {
    368        this.onStatus(true, "xhr-error");
    369      }
    370    }
    371  },
    372 
    373  async makeRequest(url, wifiData) {
    374    this.onStatus(false, "xhr-start");
    375 
    376    let fetchController = new AbortController();
    377    let fetchOpts = {
    378      method: "POST",
    379      headers: { "Content-Type": "application/json; charset=UTF-8" },
    380      credentials: "omit",
    381      signal: fetchController.signal,
    382    };
    383 
    384    if (wifiData) {
    385      fetchOpts.body = JSON.stringify({ wifiAccessPoints: wifiData });
    386    }
    387 
    388    let timeoutId = lazy.setTimeout(
    389      () => fetchController.abort(),
    390      Services.prefs.getIntPref("geo.provider.network.timeout")
    391    );
    392 
    393    let req = await fetch(url, fetchOpts);
    394    lazy.clearTimeout(timeoutId);
    395 
    396    if (!req.ok) {
    397      throw new Error(
    398        `The geolocation provider returned a non-ok status ${req.status}`,
    399        { cause: await req.text() }
    400      );
    401    }
    402 
    403    let result = req.json();
    404    return result;
    405  },
    406 };