tor-browser

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

webusb-test.js (18369B)


      1 'use strict';
      2 
      3 // This polyfill library implements the WebUSB Test API as specified here:
      4 // https://wicg.github.io/webusb/test/
      5 
      6 (() => {
      7 
      8 // These variables are logically members of the USBTest class but are defined
      9 // here to hide them from being visible as fields of navigator.usb.test.
     10 let internal = {
     11  intialized: false,
     12 
     13  webUsbService: null,
     14  webUsbServiceInterceptor: null,
     15 
     16  messagePort: null,
     17 };
     18 
     19 let mojom = {};
     20 
     21 async function loadMojomDefinitions() {
     22  const deviceMojom =
     23      await import('/gen/services/device/public/mojom/usb_device.mojom.m.js');
     24  const serviceMojom = await import(
     25      '/gen/third_party/blink/public/mojom/usb/web_usb_service.mojom.m.js');
     26  return {
     27    ...deviceMojom,
     28    ...serviceMojom,
     29  };
     30 }
     31 
     32 function getMessagePort(target) {
     33  return new Promise(resolve => {
     34    target.addEventListener('message', messageEvent => {
     35      if (messageEvent.data.type === 'ReadyForAttachment') {
     36        if (internal.messagePort === null) {
     37          internal.messagePort = messageEvent.data.port;
     38        }
     39        resolve();
     40      }
     41    }, {once: true});
     42  });
     43 }
     44 
     45 // Converts an ECMAScript String object to an instance of
     46 // mojo_base.mojom.String16.
     47 function mojoString16ToString(string16) {
     48  return String.fromCharCode.apply(null, string16.data);
     49 }
     50 
     51 // Converts an instance of mojo_base.mojom.String16 to an ECMAScript String.
     52 function stringToMojoString16(string) {
     53  let array = new Array(string.length);
     54  for (var i = 0; i < string.length; ++i) {
     55    array[i] = string.charCodeAt(i);
     56  }
     57  return { data: array }
     58 }
     59 
     60 function fakeDeviceInitToDeviceInfo(guid, init) {
     61  let deviceInfo = {
     62    guid: guid + "",
     63    usbVersionMajor: init.usbVersionMajor,
     64    usbVersionMinor: init.usbVersionMinor,
     65    usbVersionSubminor: init.usbVersionSubminor,
     66    classCode: init.deviceClass,
     67    subclassCode: init.deviceSubclass,
     68    protocolCode: init.deviceProtocol,
     69    vendorId: init.vendorId,
     70    productId: init.productId,
     71    deviceVersionMajor: init.deviceVersionMajor,
     72    deviceVersionMinor: init.deviceVersionMinor,
     73    deviceVersionSubminor: init.deviceVersionSubminor,
     74    manufacturerName: stringToMojoString16(init.manufacturerName),
     75    productName: stringToMojoString16(init.productName),
     76    serialNumber: stringToMojoString16(init.serialNumber),
     77    activeConfiguration: init.activeConfigurationValue,
     78    configurations: []
     79  };
     80  init.configurations.forEach(config => {
     81    var configInfo = {
     82      configurationValue: config.configurationValue,
     83      configurationName: stringToMojoString16(config.configurationName),
     84      selfPowered: false,
     85      remoteWakeup: false,
     86      maximumPower: 0,
     87      interfaces: [],
     88      extraData: new Uint8Array()
     89    };
     90    config.interfaces.forEach(iface => {
     91      var interfaceInfo = {
     92        interfaceNumber: iface.interfaceNumber,
     93        alternates: []
     94      };
     95      iface.alternates.forEach(alternate => {
     96        var alternateInfo = {
     97          alternateSetting: alternate.alternateSetting,
     98          classCode: alternate.interfaceClass,
     99          subclassCode: alternate.interfaceSubclass,
    100          protocolCode: alternate.interfaceProtocol,
    101          interfaceName: stringToMojoString16(alternate.interfaceName),
    102          endpoints: [],
    103          extraData: new Uint8Array()
    104        };
    105        alternate.endpoints.forEach(endpoint => {
    106          var endpointInfo = {
    107            endpointNumber: endpoint.endpointNumber,
    108            packetSize: endpoint.packetSize,
    109            synchronizationType: mojom.UsbSynchronizationType.NONE,
    110            usageType: mojom.UsbUsageType.DATA,
    111            pollingInterval: 0,
    112            extraData: new Uint8Array()
    113          };
    114          switch (endpoint.direction) {
    115            case "in":
    116              endpointInfo.direction = mojom.UsbTransferDirection.INBOUND;
    117              break;
    118            case "out":
    119              endpointInfo.direction = mojom.UsbTransferDirection.OUTBOUND;
    120              break;
    121          }
    122          switch (endpoint.type) {
    123            case "bulk":
    124              endpointInfo.type = mojom.UsbTransferType.BULK;
    125              break;
    126            case "interrupt":
    127              endpointInfo.type = mojom.UsbTransferType.INTERRUPT;
    128              break;
    129            case "isochronous":
    130              endpointInfo.type = mojom.UsbTransferType.ISOCHRONOUS;
    131              break;
    132          }
    133          alternateInfo.endpoints.push(endpointInfo);
    134        });
    135        interfaceInfo.alternates.push(alternateInfo);
    136      });
    137      configInfo.interfaces.push(interfaceInfo);
    138    });
    139    deviceInfo.configurations.push(configInfo);
    140  });
    141  return deviceInfo;
    142 }
    143 
    144 function convertMojoDeviceFilters(input) {
    145  let output = [];
    146  input.forEach(filter => {
    147    output.push(convertMojoDeviceFilter(filter));
    148  });
    149  return output;
    150 }
    151 
    152 function convertMojoDeviceFilter(input) {
    153  let output = {};
    154  if (input.hasVendorId)
    155    output.vendorId = input.vendorId;
    156  if (input.hasProductId)
    157    output.productId = input.productId;
    158  if (input.hasClassCode)
    159    output.classCode = input.classCode;
    160  if (input.hasSubclassCode)
    161    output.subclassCode = input.subclassCode;
    162  if (input.hasProtocolCode)
    163    output.protocolCode = input.protocolCode;
    164  if (input.serialNumber)
    165    output.serialNumber = mojoString16ToString(input.serialNumber);
    166  return output;
    167 }
    168 
    169 class FakeDevice {
    170  constructor(deviceInit) {
    171    this.info_ = deviceInit;
    172    this.opened_ = false;
    173    this.currentConfiguration_ = null;
    174    this.claimedInterfaces_ = new Map();
    175  }
    176 
    177  getConfiguration() {
    178    if (this.currentConfiguration_) {
    179      return Promise.resolve({
    180          value: this.currentConfiguration_.configurationValue });
    181    } else {
    182      return Promise.resolve({ value: 0 });
    183    }
    184  }
    185 
    186  open() {
    187    assert_false(this.opened_);
    188    this.opened_ = true;
    189    return Promise.resolve({result: {success: mojom.UsbOpenDeviceSuccess.OK}});
    190  }
    191 
    192  close() {
    193    assert_true(this.opened_);
    194    this.opened_ = false;
    195    return Promise.resolve();
    196  }
    197 
    198  setConfiguration(value) {
    199    assert_true(this.opened_);
    200 
    201    let selectedConfiguration = this.info_.configurations.find(
    202        configuration => configuration.configurationValue == value);
    203    // Blink should never request an invalid configuration.
    204    assert_not_equals(selectedConfiguration, undefined);
    205    this.currentConfiguration_ = selectedConfiguration;
    206    return Promise.resolve({ success: true });
    207  }
    208 
    209  async claimInterface(interfaceNumber) {
    210    assert_true(this.opened_);
    211    assert_false(this.currentConfiguration_ == null, 'device configured');
    212    assert_false(this.claimedInterfaces_.has(interfaceNumber),
    213                 'interface already claimed');
    214 
    215    const protectedInterfaces = new Set([
    216      mojom.USB_AUDIO_CLASS,
    217      mojom.USB_HID_CLASS,
    218      mojom.USB_MASS_STORAGE_CLASS,
    219      mojom.USB_SMART_CARD_CLASS,
    220      mojom.USB_VIDEO_CLASS,
    221      mojom.USB_AUDIO_VIDEO_CLASS,
    222      mojom.USB_WIRELESS_CLASS,
    223    ]);
    224 
    225    let iface = this.currentConfiguration_.interfaces.find(
    226        iface => iface.interfaceNumber == interfaceNumber);
    227    // Blink should never request an invalid interface or alternate.
    228    assert_false(iface == undefined);
    229    if (iface.alternates.some(
    230            alt => protectedInterfaces.has(alt.interfaceClass))) {
    231      return {result: mojom.UsbClaimInterfaceResult.kProtectedClass};
    232    }
    233 
    234    this.claimedInterfaces_.set(interfaceNumber, 0);
    235    return {result: mojom.UsbClaimInterfaceResult.kSuccess};
    236  }
    237 
    238  releaseInterface(interfaceNumber) {
    239    assert_true(this.opened_);
    240    assert_false(this.currentConfiguration_ == null, 'device configured');
    241    assert_true(this.claimedInterfaces_.has(interfaceNumber));
    242    this.claimedInterfaces_.delete(interfaceNumber);
    243    return Promise.resolve({ success: true });
    244  }
    245 
    246  setInterfaceAlternateSetting(interfaceNumber, alternateSetting) {
    247    assert_true(this.opened_);
    248    assert_false(this.currentConfiguration_ == null, 'device configured');
    249    assert_true(this.claimedInterfaces_.has(interfaceNumber));
    250 
    251    let iface = this.currentConfiguration_.interfaces.find(
    252        iface => iface.interfaceNumber == interfaceNumber);
    253    // Blink should never request an invalid interface or alternate.
    254    assert_false(iface == undefined);
    255    assert_true(iface.alternates.some(
    256        x => x.alternateSetting == alternateSetting));
    257    this.claimedInterfaces_.set(interfaceNumber, alternateSetting);
    258    return Promise.resolve({ success: true });
    259  }
    260 
    261  reset() {
    262    assert_true(this.opened_);
    263    return Promise.resolve({ success: true });
    264  }
    265 
    266  clearHalt(endpoint) {
    267    assert_true(this.opened_);
    268    assert_false(this.currentConfiguration_ == null, 'device configured');
    269    // TODO(reillyg): Assert that endpoint is valid.
    270    return Promise.resolve({ success: true });
    271  }
    272 
    273  async controlTransferIn(params, length, timeout) {
    274    assert_true(this.opened_);
    275 
    276    if ((params.recipient == mojom.UsbControlTransferRecipient.INTERFACE ||
    277         params.recipient == mojom.UsbControlTransferRecipient.ENDPOINT) &&
    278        this.currentConfiguration_ == null) {
    279      return {
    280        status: mojom.UsbTransferStatus.PERMISSION_DENIED,
    281      };
    282    }
    283 
    284    return {
    285      status: mojom.UsbTransferStatus.OK,
    286      data: {
    287        buffer: [
    288          length >> 8, length & 0xff, params.request, params.value >> 8,
    289          params.value & 0xff, params.index >> 8, params.index & 0xff
    290        ]
    291      }
    292    };
    293  }
    294 
    295  async controlTransferOut(params, data, timeout) {
    296    assert_true(this.opened_);
    297 
    298    if ((params.recipient == mojom.UsbControlTransferRecipient.INTERFACE ||
    299         params.recipient == mojom.UsbControlTransferRecipient.ENDPOINT) &&
    300        this.currentConfiguration_ == null) {
    301      return {
    302        status: mojom.UsbTransferStatus.PERMISSION_DENIED,
    303      };
    304    }
    305 
    306    return {status: mojom.UsbTransferStatus.OK, bytesWritten: data.byteLength};
    307  }
    308 
    309  genericTransferIn(endpointNumber, length, timeout) {
    310    assert_true(this.opened_);
    311    assert_false(this.currentConfiguration_ == null, 'device configured');
    312    // TODO(reillyg): Assert that endpoint is valid.
    313    let data = new Array(length);
    314    for (let i = 0; i < length; ++i)
    315      data[i] = i & 0xff;
    316    return Promise.resolve(
    317        {status: mojom.UsbTransferStatus.OK, data: {buffer: data}});
    318  }
    319 
    320  genericTransferOut(endpointNumber, data, timeout) {
    321    assert_true(this.opened_);
    322    assert_false(this.currentConfiguration_ == null, 'device configured');
    323    // TODO(reillyg): Assert that endpoint is valid.
    324    return Promise.resolve(
    325        {status: mojom.UsbTransferStatus.OK, bytesWritten: data.byteLength});
    326  }
    327 
    328  isochronousTransferIn(endpointNumber, packetLengths, timeout) {
    329    assert_true(this.opened_);
    330    assert_false(this.currentConfiguration_ == null, 'device configured');
    331    // TODO(reillyg): Assert that endpoint is valid.
    332    let data = new Array(packetLengths.reduce((a, b) => a + b, 0));
    333    let dataOffset = 0;
    334    let packets = new Array(packetLengths.length);
    335    for (let i = 0; i < packetLengths.length; ++i) {
    336      for (let j = 0; j < packetLengths[i]; ++j)
    337        data[dataOffset++] = j & 0xff;
    338      packets[i] = {
    339        length: packetLengths[i],
    340        transferredLength: packetLengths[i],
    341        status: mojom.UsbTransferStatus.OK
    342      };
    343    }
    344    return Promise.resolve({data: {buffer: data}, packets: packets});
    345  }
    346 
    347  isochronousTransferOut(endpointNumber, data, packetLengths, timeout) {
    348    assert_true(this.opened_);
    349    assert_false(this.currentConfiguration_ == null, 'device configured');
    350    // TODO(reillyg): Assert that endpoint is valid.
    351    let packets = new Array(packetLengths.length);
    352    for (let i = 0; i < packetLengths.length; ++i) {
    353      packets[i] = {
    354        length: packetLengths[i],
    355        transferredLength: packetLengths[i],
    356        status: mojom.UsbTransferStatus.OK
    357      };
    358    }
    359    return Promise.resolve({ packets: packets });
    360  }
    361 }
    362 
    363 class FakeWebUsbService {
    364  constructor() {
    365    this.receiver_ = new mojom.WebUsbServiceReceiver(this);
    366    this.devices_ = new Map();
    367    this.devicesByGuid_ = new Map();
    368    this.client_ = null;
    369    this.nextGuid_ = 0;
    370  }
    371 
    372  addBinding(handle) {
    373    this.receiver_.$.bindHandle(handle);
    374  }
    375 
    376  addDevice(fakeDevice, info) {
    377    let device = {
    378      fakeDevice: fakeDevice,
    379      guid: (this.nextGuid_++).toString(),
    380      info: info,
    381      receivers: [],
    382    };
    383    this.devices_.set(fakeDevice, device);
    384    this.devicesByGuid_.set(device.guid, device);
    385    if (this.client_)
    386      this.client_.onDeviceAdded(fakeDeviceInitToDeviceInfo(device.guid, info));
    387  }
    388 
    389  async forgetDevice(guid) {
    390    // Permissions are currently untestable through WPT.
    391  }
    392 
    393  removeDevice(fakeDevice) {
    394    let device = this.devices_.get(fakeDevice);
    395    if (!device)
    396      throw new Error('Cannot remove unknown device.');
    397 
    398    for (const receiver of device.receivers)
    399      receiver.$.close();
    400    this.devices_.delete(device.fakeDevice);
    401    this.devicesByGuid_.delete(device.guid);
    402    if (this.client_) {
    403      this.client_.onDeviceRemoved(
    404          fakeDeviceInitToDeviceInfo(device.guid, device.info));
    405    }
    406  }
    407 
    408  removeAllDevices() {
    409    this.devices_.forEach(device => {
    410      for (const receiver of device.receivers)
    411        receiver.$.close();
    412      this.client_.onDeviceRemoved(
    413          fakeDeviceInitToDeviceInfo(device.guid, device.info));
    414    });
    415    this.devices_.clear();
    416    this.devicesByGuid_.clear();
    417  }
    418 
    419  getDevices() {
    420    let devices = [];
    421    this.devices_.forEach(device => {
    422      devices.push(fakeDeviceInitToDeviceInfo(device.guid, device.info));
    423    });
    424    return Promise.resolve({ results: devices });
    425  }
    426 
    427  getDevice(guid, request) {
    428    let retrievedDevice = this.devicesByGuid_.get(guid);
    429    if (retrievedDevice) {
    430      const receiver =
    431          new mojom.UsbDeviceReceiver(new FakeDevice(retrievedDevice.info));
    432      receiver.$.bindHandle(request.handle);
    433      receiver.onConnectionError.addListener(() => {
    434        if (retrievedDevice.fakeDevice.onclose)
    435          retrievedDevice.fakeDevice.onclose();
    436      });
    437      retrievedDevice.receivers.push(receiver);
    438    } else {
    439      request.handle.close();
    440    }
    441  }
    442 
    443  getPermission(options) {
    444    return new Promise(resolve => {
    445      if (navigator.usb.test.onrequestdevice) {
    446        navigator.usb.test.onrequestdevice(
    447            new USBDeviceRequestEvent(options, resolve));
    448      } else {
    449        resolve({ result: null });
    450      }
    451    });
    452  }
    453 
    454  setClient(client) {
    455    this.client_ = client;
    456  }
    457 }
    458 
    459 class USBDeviceRequestEvent {
    460  constructor(options, resolve) {
    461    this.filters = convertMojoDeviceFilters(options.filters);
    462    this.exclusionFilters = convertMojoDeviceFilters(options.exclusionFilters);
    463    this.resolveFunc_ = resolve;
    464  }
    465 
    466  respondWith(value) {
    467    // Wait until |value| resolves (if it is a Promise). This function returns
    468    // no value.
    469    Promise.resolve(value).then(fakeDevice => {
    470      let device = internal.webUsbService.devices_.get(fakeDevice);
    471      let result = null;
    472      if (device) {
    473        result = fakeDeviceInitToDeviceInfo(device.guid, device.info);
    474      }
    475      this.resolveFunc_({ result: result });
    476    }, () => {
    477      this.resolveFunc_({ result: null });
    478    });
    479  }
    480 }
    481 
    482 // Unlike FakeDevice this class is exported to callers of USBTest.addFakeDevice.
    483 class FakeUSBDevice {
    484  constructor() {
    485    this.onclose = null;
    486  }
    487 
    488  disconnect() {
    489    setTimeout(() => internal.webUsbService.removeDevice(this), 0);
    490  }
    491 }
    492 
    493 class USBTest {
    494  constructor() {
    495    this.onrequestdevice = undefined;
    496  }
    497 
    498  async initialize() {
    499    if (internal.initialized)
    500      return;
    501 
    502    // Be ready to handle 'ReadyForAttachment' message from child iframes.
    503    if ('window' in self) {
    504      getMessagePort(window);
    505    }
    506 
    507    mojom = await loadMojomDefinitions();
    508    internal.webUsbService = new FakeWebUsbService();
    509    internal.webUsbServiceInterceptor =
    510        new MojoInterfaceInterceptor(mojom.WebUsbService.$interfaceName);
    511    internal.webUsbServiceInterceptor.oninterfacerequest =
    512        e => internal.webUsbService.addBinding(e.handle);
    513    internal.webUsbServiceInterceptor.start();
    514 
    515    // Wait for a call to GetDevices() to pass between the renderer and the
    516    // mock in order to establish that everything is set up.
    517    await navigator.usb.getDevices();
    518    internal.initialized = true;
    519  }
    520 
    521  // Returns a promise that is resolved when the implementation of |usb| in the
    522  // global scope for |context| is controlled by the current context.
    523  attachToContext(context) {
    524    if (!internal.initialized)
    525      throw new Error('Call initialize() before attachToContext()');
    526 
    527    let target = context.constructor.name === 'Worker' ? context : window;
    528    return getMessagePort(target).then(() => {
    529      return new Promise(resolve => {
    530        internal.messagePort.onmessage = channelEvent => {
    531          switch (channelEvent.data.type) {
    532            case mojom.WebUsbService.$interfaceName:
    533              internal.webUsbService.addBinding(channelEvent.data.handle);
    534              break;
    535            case 'Complete':
    536              resolve();
    537              break;
    538          }
    539        };
    540        internal.messagePort.postMessage({
    541          type: 'Attach',
    542          interfaces: [
    543            mojom.WebUsbService.$interfaceName,
    544          ]
    545        });
    546      });
    547    });
    548  }
    549 
    550  addFakeDevice(deviceInit) {
    551    if (!internal.initialized)
    552      throw new Error('Call initialize() before addFakeDevice().');
    553 
    554    // |addDevice| and |removeDevice| are called in a setTimeout callback so
    555    // that tests do not rely on the device being immediately available which
    556    // may not be true for all implementations of this test API.
    557    let fakeDevice = new FakeUSBDevice();
    558    setTimeout(
    559        () => internal.webUsbService.addDevice(fakeDevice, deviceInit), 0);
    560    return fakeDevice;
    561  }
    562 
    563  reset() {
    564    if (!internal.initialized)
    565      throw new Error('Call initialize() before reset().');
    566 
    567    // Reset the mocks in a setTimeout callback so that tests do not rely on
    568    // the fact that this polyfill can do this synchronously.
    569    return new Promise(resolve => {
    570      setTimeout(() => {
    571        if (internal.messagePort !== null)
    572          internal.messagePort.close();
    573        internal.messagePort = null;
    574        internal.webUsbService.removeAllDevices();
    575        resolve();
    576      }, 0);
    577    });
    578  }
    579 }
    580 
    581 navigator.usb.test = new USBTest();
    582 
    583 })();