tor-browser

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

discovery.js (12372B)


      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 "use strict";
      5 
      6 /**
      7 * This implements a UDP mulitcast device discovery protocol that:
      8 *   * Is optimized for mobile devices
      9 *   * Doesn't require any special schema for service info
     10 *
     11 * To ensure it works well on mobile devices, there is no heartbeat or other
     12 * recurring transmission.
     13 *
     14 * Devices are typically in one of two groups: scanning for services or
     15 * providing services (though they may be in both groups as well).
     16 *
     17 * Scanning devices listen on UPDATE_PORT for UDP multicast traffic.  When the
     18 * scanning device wants to force an update of the services available, it sends
     19 * a status packet to SCAN_PORT.
     20 *
     21 * Service provider devices listen on SCAN_PORT for any packets from scanning
     22 * devices.  If one is recevied, the provider device sends a status packet
     23 * (listing the services it offers) to UPDATE_PORT.
     24 *
     25 * Scanning devices purge any previously known devices after REPLY_TIMEOUT ms
     26 * from that start of a scan if no reply is received during the most recent
     27 * scan.
     28 *
     29 * When a service is registered, is supplies a regular object with any details
     30 * about itself (a port number, for example) in a service-defined format, which
     31 * is then available to scanning devices.
     32 */
     33 
     34 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     35 
     36 const UDPSocket = Components.Constructor(
     37  "@mozilla.org/network/udp-socket;1",
     38  "nsIUDPSocket",
     39  "init"
     40 );
     41 
     42 const SCAN_PORT = 50624;
     43 const UPDATE_PORT = 50625;
     44 const ADDRESS = "224.0.0.115";
     45 const REPLY_TIMEOUT = 5000;
     46 
     47 var logging = Services.prefs.getBoolPref("devtools.discovery.log");
     48 function log(msg) {
     49  if (logging) {
     50    console.log("DISCOVERY: " + msg);
     51  }
     52 }
     53 
     54 /**
     55 * Each Transport instance owns a single UDPSocket.
     56 */
     57 class Transport extends EventEmitter {
     58  /**
     59   * @param {Integer} port
     60   *        The port to listen on for incoming UDP multicast packets.
     61   */
     62  constructor(port) {
     63    super();
     64 
     65    try {
     66      this.socket = new UDPSocket(
     67        port,
     68        false,
     69        Services.scriptSecurityManager.getSystemPrincipal()
     70      );
     71      this.socket.joinMulticast(ADDRESS);
     72      this.socket.asyncListen(this);
     73    } catch (e) {
     74      log("Failed to start new socket: " + e);
     75    }
     76  }
     77  /**
     78   * Send a object to some UDP port.
     79   *
     80   * @param {object} object
     81   *        Object which is the message to send
     82   * @param {Integer} port
     83   *        UDP port to send the message to
     84   */
     85  send(object, port) {
     86    if (logging) {
     87      log("Send to " + port + ":\n" + JSON.stringify(object, null, 2));
     88    }
     89    const message = JSON.stringify(object);
     90    const rawMessage = Uint8Array.from(message, x => x.charCodeAt(0));
     91    try {
     92      this.socket.send(ADDRESS, port, rawMessage, rawMessage.length);
     93    } catch (e) {
     94      log("Failed to send message: " + e);
     95    }
     96  }
     97 
     98  destroy() {
     99    this.socket.close();
    100  }
    101 
    102  // nsIUDPSocketListener
    103  onPacketReceived(socket, message) {
    104    const messageData = message.data;
    105    const object = JSON.parse(messageData);
    106    object.from = message.fromAddr.address;
    107    const port = message.fromAddr.port;
    108    if (port == this.socket.port) {
    109      log("Ignoring looped message");
    110      return;
    111    }
    112    if (logging) {
    113      log(
    114        "Recv on " + this.socket.port + ":\n" + JSON.stringify(object, null, 2)
    115      );
    116    }
    117    this.emit("message", object);
    118  }
    119 
    120  onStopListening() {}
    121 }
    122 
    123 /**
    124 * Manages the local device's name.  The name can be generated in serveral
    125 * platform-specific ways (see |_generate|).  The aim is for each device on the
    126 * same local network to have a unique name.
    127 */
    128 class LocalDevice {
    129  static UNKNOWN = "unknown";
    130  constructor() {
    131    this._name = LocalDevice.UNKNOWN;
    132    // Trigger |_get| to load name eagerly
    133    this._get();
    134  }
    135  _get() {
    136    // Without Settings API, just generate a name and stop, since the value
    137    // can't be persisted.
    138    this._generate();
    139  }
    140 
    141  /**
    142   * Generate a new device name from various platform-specific properties.
    143   * Triggers the |name| setter to persist if needed.
    144   */
    145  _generate() {
    146    if (Services.appinfo.widgetToolkit == "android") {
    147      // For Firefox for Android, use the device's model name.
    148      // TODO: Bug 1180997: Find the right way to expose an editable name
    149      this.name = Services.sysinfo.get("device");
    150    } else {
    151      this.name = Services.dns.myHostName;
    152    }
    153  }
    154 
    155  get name() {
    156    return this._name;
    157  }
    158 
    159  set name(name) {
    160    this._name = name;
    161    log("Device: " + this._name);
    162  }
    163 }
    164 
    165 class Discovery extends EventEmitter {
    166  constructor() {
    167    super();
    168 
    169    this.localServices = {};
    170    this.remoteServices = {};
    171    this.device = new LocalDevice();
    172    this.replyTimeout = REPLY_TIMEOUT;
    173 
    174    // Defaulted to Transport, but can be altered by tests
    175    this._factories = { Transport };
    176 
    177    this._transports = {
    178      scan: null,
    179      update: null,
    180    };
    181    this._expectingReplies = {
    182      from: new Set(),
    183    };
    184 
    185    this._onRemoteScan = this._onRemoteScan.bind(this);
    186    this._onRemoteUpdate = this._onRemoteUpdate.bind(this);
    187    this._purgeMissingDevices = this._purgeMissingDevices.bind(this);
    188  }
    189  /**
    190   * Add a new service offered by this device.
    191   *
    192   * @param {string} service
    193   *        Name of the service
    194   * @param {object} info
    195   *        Arbitrary data about the service to announce to scanning devices
    196   */
    197  addService(service, info) {
    198    log("ADDING LOCAL SERVICE");
    199    if (Object.keys(this.localServices).length === 0) {
    200      this._startListeningForScan();
    201    }
    202    this.localServices[service] = info;
    203  }
    204 
    205  /**
    206   * Remove a service offered by this device.
    207   *
    208   * @param {string} service
    209   *        Name of the service
    210   */
    211  removeService(service) {
    212    delete this.localServices[service];
    213    if (Object.keys(this.localServices).length === 0) {
    214      this._stopListeningForScan();
    215    }
    216  }
    217 
    218  /**
    219   * Scan for service updates from other devices.
    220   */
    221  scan() {
    222    this._startListeningForUpdate();
    223    this._waitForReplies();
    224    // TODO Bug 1027457: Use timer to debounce
    225    this._sendStatusTo(SCAN_PORT);
    226  }
    227 
    228  /**
    229   * Get a list of all remote devices currently offering some service.
    230   */
    231  getRemoteDevices() {
    232    const devices = new Set();
    233    for (const service in this.remoteServices) {
    234      for (const device in this.remoteServices[service]) {
    235        devices.add(device);
    236      }
    237    }
    238    return [...devices];
    239  }
    240 
    241  /**
    242   * Get a list of all remote devices currently offering a particular service.
    243   */
    244  getRemoteDevicesWithService(service) {
    245    const devicesWithService = this.remoteServices[service] || {};
    246    return Object.keys(devicesWithService);
    247  }
    248 
    249  /**
    250   * Get service info (any details registered by the remote device) for a given
    251   * service on a device.
    252   */
    253  getRemoteService(service, device) {
    254    const devicesWithService = this.remoteServices[service] || {};
    255    return devicesWithService[device];
    256  }
    257 
    258  _waitForReplies() {
    259    clearTimeout(this._expectingReplies.timer);
    260    this._expectingReplies.from = new Set(this.getRemoteDevices());
    261    this._expectingReplies.timer = setTimeout(
    262      this._purgeMissingDevices,
    263      this.replyTimeout
    264    );
    265  }
    266 
    267  get Transport() {
    268    return this._factories.Transport;
    269  }
    270 
    271  _startListeningForScan() {
    272    if (this._transports.scan) {
    273      // Already listening
    274      return;
    275    }
    276    log("LISTEN FOR SCAN");
    277    this._transports.scan = new this.Transport(SCAN_PORT);
    278    this._transports.scan.on("message", this._onRemoteScan);
    279  }
    280 
    281  _stopListeningForScan() {
    282    if (!this._transports.scan) {
    283      // Not listening
    284      return;
    285    }
    286    this._transports.scan.off("message", this._onRemoteScan);
    287    this._transports.scan.destroy();
    288    this._transports.scan = null;
    289  }
    290 
    291  _startListeningForUpdate() {
    292    if (this._transports.update) {
    293      // Already listening
    294      return;
    295    }
    296    log("LISTEN FOR UPDATE");
    297    this._transports.update = new this.Transport(UPDATE_PORT);
    298    this._transports.update.on("message", this._onRemoteUpdate);
    299  }
    300 
    301  _stopListeningForUpdate() {
    302    if (!this._transports.update) {
    303      // Not listening
    304      return;
    305    }
    306    this._transports.update.off("message", this._onRemoteUpdate);
    307    this._transports.update.destroy();
    308    this._transports.update = null;
    309  }
    310 
    311  _restartListening() {
    312    if (this._transports.scan) {
    313      this._stopListeningForScan();
    314      this._startListeningForScan();
    315    }
    316    if (this._transports.update) {
    317      this._stopListeningForUpdate();
    318      this._startListeningForUpdate();
    319    }
    320  }
    321 
    322  /**
    323   * When sending message, we can use either transport, so just pick the first
    324   * one currently alive.
    325   */
    326  get _outgoingTransport() {
    327    if (this._transports.scan) {
    328      return this._transports.scan;
    329    }
    330    if (this._transports.update) {
    331      return this._transports.update;
    332    }
    333    return null;
    334  }
    335 
    336  _sendStatusTo(port) {
    337    const status = {
    338      device: this.device.name,
    339      services: this.localServices,
    340    };
    341    this._outgoingTransport.send(status, port);
    342  }
    343 
    344  _onRemoteScan() {
    345    // Send my own status in response
    346    log("GOT SCAN REQUEST");
    347    this._sendStatusTo(UPDATE_PORT);
    348  }
    349 
    350  _onRemoteUpdate(update) {
    351    log("GOT REMOTE UPDATE");
    352 
    353    const remoteDevice = update.device;
    354    const remoteHost = update.from;
    355 
    356    // Record the reply as received so it won't be purged as missing
    357    this._expectingReplies.from.delete(remoteDevice);
    358 
    359    // First, loop over the known services
    360    for (const service in this.remoteServices) {
    361      const devicesWithService = this.remoteServices[service];
    362      const hadServiceForDevice = !!devicesWithService[remoteDevice];
    363      const haveServiceForDevice = service in update.services;
    364      // If the remote device used to have service, but doesn't any longer, then
    365      // it was deleted, so we remove it here.
    366      if (hadServiceForDevice && !haveServiceForDevice) {
    367        delete devicesWithService[remoteDevice];
    368        log("REMOVED " + service + ", DEVICE " + remoteDevice);
    369        this.emit(service + "-device-removed", remoteDevice);
    370      }
    371    }
    372 
    373    // Second, loop over the services in the received update
    374    for (const service in update.services) {
    375      // Detect if this is a new device for this service
    376      const newDevice =
    377        !this.remoteServices[service] ||
    378        !this.remoteServices[service][remoteDevice];
    379 
    380      // Look up the service info we may have received previously from the same
    381      // remote device
    382      const devicesWithService = this.remoteServices[service] || {};
    383      const oldDeviceInfo = devicesWithService[remoteDevice];
    384 
    385      // Store the service info from the remote device
    386      const newDeviceInfo = Cu.cloneInto(update.services[service], {});
    387      newDeviceInfo.host = remoteHost;
    388      devicesWithService[remoteDevice] = newDeviceInfo;
    389      this.remoteServices[service] = devicesWithService;
    390 
    391      // If this is a new service for the remote device, announce the addition
    392      if (newDevice) {
    393        log("ADDED " + service + ", DEVICE " + remoteDevice);
    394        this.emit(service + "-device-added", remoteDevice, newDeviceInfo);
    395      }
    396 
    397      // If we've seen this service from the remote device, but the details have
    398      // changed, announce the update
    399      if (
    400        !newDevice &&
    401        JSON.stringify(oldDeviceInfo) != JSON.stringify(newDeviceInfo)
    402      ) {
    403        log("UPDATED " + service + ", DEVICE " + remoteDevice);
    404        this.emit(service + "-device-updated", remoteDevice, newDeviceInfo);
    405      }
    406    }
    407  }
    408 
    409  _purgeMissingDevices() {
    410    log("PURGING MISSING DEVICES");
    411    for (const service in this.remoteServices) {
    412      const devicesWithService = this.remoteServices[service];
    413      for (const remoteDevice in devicesWithService) {
    414        // If we're still expecting a reply from a remote device when it's time
    415        // to purge, then the service is removed.
    416        if (this._expectingReplies.from.has(remoteDevice)) {
    417          delete devicesWithService[remoteDevice];
    418          log("REMOVED " + service + ", DEVICE " + remoteDevice);
    419          this.emit(service + "-device-removed", remoteDevice);
    420        }
    421      }
    422    }
    423  }
    424 }
    425 
    426 var discovery = new Discovery();
    427 
    428 module.exports = discovery;