UNPKG

fakebrowser

Version:

🤖 Fake fingerprints to bypass anti-bot systems. Simulate mouse and keyboard operations to make behavior like a real person.

1,164 lines (1,061 loc) 63.5 kB
// noinspection JSUnusedLocalSymbols 'use strict'; const {PuppeteerExtraPlugin} = require('puppeteer-extra-plugin'); const withUtils = require('../_utils/withUtils'); const withWorkerUtils = require('../_utils/withWorkerUtils'); // bluetooth can only be supported in linux by turning on the switch "--enable-experimental-web-platform-features" // However, when the experimental turns on, the properties of window, document, navigator will be polluted by new parameters // so the version number of the browser does not correspond to it. // We need to implement bluetooth class manually: // // We should strictly follow this definition and then insert the window to avoid detected by creepjs as being in wrong order // Just like he detects the position of chrome in window :D // "Bluetooth" // "BluetoothCharacteristicProperties" // "BluetoothDevice" // "BluetoothRemoteGATTCharacteristic" // "BluetoothRemoteGATTDescriptor" // "BluetoothRemoteGATTServer" // "BluetoothRemoteGATTService" // "BluetoothUUID" // https://github.com/WebBluetoothCG/web-bluetooth // thanks to: // https://github.com/thegecko/webbluetooth/blob/master/src/helpers.ts class Plugin extends PuppeteerExtraPlugin { constructor(opts = {}) { super(opts); } get name() { return 'evasions/bluetooth'; } async onPageCreated(page) { await withUtils(this, page).evaluateOnNewDocument(this.mainFunction); } // onServiceWorkerContent(jsContent) { // return withWorkerUtils(this, jsContent).evaluate(this.mainFunction); // } mainFunction = (utils) => { if ('undefined' !== typeof window.Bluetooth) { return; } const _Object = utils.cache.Object; const _Reflect = utils.cache.Reflect; /** * Known services enum */ const bluetoothServices = { 'alert_notification': 0x1811, 'automation_io': 0x1815, 'battery_service': 0x180F, 'blood_pressure': 0x1810, 'body_composition': 0x181B, 'bond_management': 0x181E, 'continuous_glucose_monitoring': 0x181F, 'current_time': 0x1805, 'cycling_power': 0x1818, 'cycling_speed_and_cadence': 0x1816, 'device_information': 0x180A, 'environmental_sensing': 0x181A, 'generic_access': 0x1800, 'generic_attribute': 0x1801, 'glucose': 0x1808, 'health_thermometer': 0x1809, 'heart_rate': 0x180D, 'human_interface_device': 0x1812, 'immediate_alert': 0x1802, 'indoor_positioning': 0x1821, 'internet_protocol_support': 0x1820, 'link_loss': 0x1803, 'location_and_navigation': 0x1819, 'next_dst_change': 0x1807, 'phone_alert_status': 0x180E, 'pulse_oximeter': 0x1822, 'reference_time_update': 0x1806, 'running_speed_and_cadence': 0x1814, 'scan_parameters': 0x1813, 'tx_power': 0x1804, 'user_data': 0x181C, 'weight_scale': 0x181D, }; /** * Known characteristics enum */ const bluetoothCharacteristics = { 'aerobic_heart_rate_lower_limit': 0x2A7E, 'aerobic_heart_rate_upper_limit': 0x2A84, 'aerobic_threshold': 0x2A7F, 'age': 0x2A80, 'aggregate': 0x2A5A, 'alert_category_id': 0x2A43, 'alert_category_id_bit_mask': 0x2A42, 'alert_level': 0x2A06, 'alert_notification_control_point': 0x2A44, 'alert_status': 0x2A3F, 'altitude': 0x2AB3, 'anaerobic_heart_rate_lower_limit': 0x2A81, 'anaerobic_heart_rate_upper_limit': 0x2A82, 'anaerobic_threshold': 0x2A83, 'analog': 0x2A58, 'apparent_wind_direction': 0x2A73, 'apparent_wind_speed': 0x2A72, 'gap.appearance': 0x2A01, 'barometric_pressure_trend': 0x2AA3, 'battery_level': 0x2A19, 'blood_pressure_feature': 0x2A49, 'blood_pressure_measurement': 0x2A35, 'body_composition_feature': 0x2A9B, 'body_composition_measurement': 0x2A9C, 'body_sensor_location': 0x2A38, 'bond_management_control_point': 0x2AA4, 'bond_management_feature': 0x2AA5, 'boot_keyboard_input_report': 0x2A22, 'boot_keyboard_output_report': 0x2A32, 'boot_mouse_input_report': 0x2A33, 'gap.central_address_resolution_support': 0x2AA6, 'cgm_feature': 0x2AA8, 'cgm_measurement': 0x2AA7, 'cgm_session_run_time': 0x2AAB, 'cgm_session_start_time': 0x2AAA, 'cgm_specific_ops_control_point': 0x2AAC, 'cgm_status': 0x2AA9, 'csc_feature': 0x2A5C, 'csc_measurement': 0x2A5B, 'current_time': 0x2A2B, 'cycling_power_control_point': 0x2A66, 'cycling_power_feature': 0x2A65, 'cycling_power_measurement': 0x2A63, 'cycling_power_vector': 0x2A64, 'database_change_increment': 0x2A99, 'date_of_birth': 0x2A85, 'date_of_threshold_assessment': 0x2A86, 'date_time': 0x2A08, 'day_date_time': 0x2A0A, 'day_of_week': 0x2A09, 'descriptor_value_changed': 0x2A7D, 'gap.device_name': 0x2A00, 'dew_point': 0x2A7B, 'digital': 0x2A56, 'dst_offset': 0x2A0D, 'elevation': 0x2A6C, 'email_address': 0x2A87, 'exact_time_256': 0x2A0C, 'fat_burn_heart_rate_lower_limit': 0x2A88, 'fat_burn_heart_rate_upper_limit': 0x2A89, 'firmware_revision_string': 0x2A26, 'first_name': 0x2A8A, 'five_zone_heart_rate_limits': 0x2A8B, 'floor_number': 0x2AB2, 'gender': 0x2A8C, 'glucose_feature': 0x2A51, 'glucose_measurement': 0x2A18, 'glucose_measurement_context': 0x2A34, 'gust_factor': 0x2A74, 'hardware_revision_string': 0x2A27, 'heart_rate_control_point': 0x2A39, 'heart_rate_max': 0x2A8D, 'heart_rate_measurement': 0x2A37, 'heat_index': 0x2A7A, 'height': 0x2A8E, 'hid_control_point': 0x2A4C, 'hid_information': 0x2A4A, 'hip_circumference': 0x2A8F, 'humidity': 0x2A6F, 'ieee_11073-20601_regulatory_certification_data_list': 0x2A2A, 'indoor_positioning_configuration': 0x2AAD, 'intermediate_blood_pressure': 0x2A36, 'intermediate_temperature': 0x2A1E, 'irradiance': 0x2A77, 'language': 0x2AA2, 'last_name': 0x2A90, 'latitude': 0x2AAE, 'ln_control_point': 0x2A6B, 'ln_feature': 0x2A6A, 'local_east_coordinate.xml': 0x2AB1, 'local_north_coordinate': 0x2AB0, 'local_time_information': 0x2A0F, 'location_and_speed': 0x2A67, 'location_name': 0x2AB5, 'longitude': 0x2AAF, 'magnetic_declination': 0x2A2C, 'magnetic_flux_density_2D': 0x2AA0, 'magnetic_flux_density_3D': 0x2AA1, 'manufacturer_name_string': 0x2A29, 'maximum_recommended_heart_rate': 0x2A91, 'measurement_interval': 0x2A21, 'model_number_string': 0x2A24, 'navigation': 0x2A68, 'new_alert': 0x2A46, 'gap.peripheral_preferred_connection_parameters': 0x2A04, 'gap.peripheral_privacy_flag': 0x2A02, 'plx_continuous_measurement': 0x2A5F, 'plx_features': 0x2A60, 'plx_spot_check_measurement': 0x2A5E, 'pnp_id': 0x2A50, 'pollen_concentration': 0x2A75, 'position_quality': 0x2A69, 'pressure': 0x2A6D, 'protocol_mode': 0x2A4E, 'rainfall': 0x2A78, 'gap.reconnection_address': 0x2A03, 'record_access_control_point': 0x2A52, 'reference_time_information': 0x2A14, 'report': 0x2A4D, 'report_map': 0x2A4B, 'resting_heart_rate': 0x2A92, 'ringer_control_point': 0x2A40, 'ringer_setting': 0x2A41, 'rsc_feature': 0x2A54, 'rsc_measurement': 0x2A53, 'sc_control_point': 0x2A55, 'scan_interval_window': 0x2A4F, 'scan_refresh': 0x2A31, 'sensor_location': 0x2A5D, 'serial_number_string': 0x2A25, 'gatt.service_changed': 0x2A05, 'software_revision_string': 0x2A28, 'sport_type_for_aerobic_and_anaerobic_thresholds': 0x2A93, 'supported_new_alert_category': 0x2A47, 'supported_unread_alert_category': 0x2A48, 'system_id': 0x2A23, 'temperature': 0x2A6E, 'temperature_measurement': 0x2A1C, 'temperature_type': 0x2A1D, 'three_zone_heart_rate_limits': 0x2A94, 'time_accuracy': 0x2A12, 'time_source': 0x2A13, 'time_update_control_point': 0x2A16, 'time_update_state': 0x2A17, 'time_with_dst': 0x2A11, 'time_zone': 0x2A0E, 'true_wind_direction': 0x2A71, 'true_wind_speed': 0x2A70, 'two_zone_heart_rate_limit': 0x2A95, 'tx_power_level': 0x2A07, 'uncertainty': 0x2AB4, 'unread_alert_status': 0x2A45, 'user_control_point': 0x2A9F, 'user_index': 0x2A9A, 'uv_index': 0x2A76, 'vo2_max': 0x2A96, 'waist_circumference': 0x2A97, 'weight': 0x2A98, 'weight_measurement': 0x2A9D, 'weight_scale_feature': 0x2A9E, 'wind_chill': 0x2A79, }; /** * Known descriptors enum */ const bluetoothDescriptors = { 'gatt.characteristic_extended_properties': 0x2900, 'gatt.characteristic_user_description': 0x2901, 'gatt.client_characteristic_configuration': 0x2902, 'gatt.server_characteristic_configuration': 0x2903, 'gatt.characteristic_presentation_format': 0x2904, 'gatt.characteristic_aggregate_format': 0x2905, 'valid_range': 0x2906, 'external_report_reference': 0x2907, 'report_reference': 0x2908, 'number_of_digitals': 0x2909, 'value_trigger_setting': 0x290A, 'es_configuration': 0x290B, 'es_measurement': 0x290C, 'es_trigger_setting': 0x290D, 'time_trigger_setting': 0x290E, }; /** * Gets a canonical UUID from a partial UUID in string or hex format * @param uuid The partial UUID * @returns canonical UUID */ function getCanonicalUUID(uuid) { if (typeof uuid === 'number') uuid = uuid.toString(16); uuid = uuid.toLowerCase(); if (uuid.length <= 8) uuid = ('00000000' + uuid).slice(-8) + '-0000-1000-8000-00805f9b34fb'; if (uuid.length === 32) uuid = uuid.match(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/).splice(1).join('-'); return uuid; } /** * Gets a canonical service UUID from a known service name or partial UUID in string or hex format * @param service The known service name * @returns canonical UUID */ function getServiceUUID(service) { // Check for string as enums also allow a reverse lookup which will match any numbers passed in if (typeof service === 'string' && bluetoothServices[service]) { service = bluetoothServices[service]; } return getCanonicalUUID(service); } /** * Gets a canonical characteristic UUID from a known characteristic name or partial UUID in string or hex format * @param characteristic The known characteristic name * @returns canonical UUID */ function getCharacteristicUUID(characteristic) { // Check for string as enums also allow a reverse lookup which will match any numbers passed in if (typeof characteristic === 'string' && bluetoothCharacteristics[characteristic]) { characteristic = bluetoothCharacteristics[characteristic]; } return getCanonicalUUID(characteristic); } /** * Gets a canonical descriptor UUID from a known descriptor name or partial UUID in string or hex format * @param descriptor The known descriptor name * @returns canonical UUID */ function getDescriptorUUID(descriptor) { // Check for string as enums also allow a reverse lookup which will match any numbers passed in if (typeof descriptor === 'string' && bluetoothDescriptors[descriptor]) { descriptor = bluetoothDescriptors[descriptor]; } return getCanonicalUUID(descriptor); } // Define types // "Bluetooth" // "BluetoothCharacteristicProperties" // "BluetoothDevice" // "BluetoothRemoteGATTCharacteristic" // "BluetoothRemoteGATTDescriptor" // "BluetoothRemoteGATTServer" // "BluetoothRemoteGATTService" // "BluetoothUUID" let navigatorBluetoothHasSet = false; const BluetoothPseudo = function () { if (navigatorBluetoothHasSet) { throw utils.patchError(new TypeError(`Illegal constructor`), 'construct'); } }; const fakeBluetoothInstance = new BluetoothPseudo(); utils.makePseudoClass(window, 'Bluetooth', BluetoothPseudo, EventTarget); utils.makePseudoClass(window, 'BluetoothCharacteristicProperties', null, null); utils.makePseudoClass(window, 'BluetoothDevice', null, EventTarget); utils.makePseudoClass(window, 'BluetoothRemoteGATTCharacteristic', null, EventTarget); utils.makePseudoClass(window, 'BluetoothRemoteGATTDescriptor', null, null); utils.makePseudoClass(window, 'BluetoothRemoteGATTServer', null, null); utils.makePseudoClass(window, 'BluetoothRemoteGATTService', null, null); utils.makePseudoClass(window, 'BluetoothUUID', null, null); // ============================================================================================== // BluetoothUUID // BluetoothUUID.canonicalUUID // BluetoothUUID.getCharacteristic // BluetoothUUID.getDescriptor // BluetoothUUID.getService // BluetoothUUID.prototype.[Symbol.toStringTag] utils.mockWithProxy( window.BluetoothUUID, 'canonicalUUID', _Object.create, { configurable: true, enumerable: true, writable: true, }, { get(target, property, receiver) { if (property === 'name') { return 'canonicalUUID'; } if (property === 'length') { return 1; } return _Reflect.get(target, property, receiver); }, apply(target, thisArg, args) { if (args.length === 0) { throw utils.patchError( new TypeError(`Failed to execute 'canonicalUUID' on 'BluetoothUUID': 1 argument required, but only 0 present.`), 'canonicalUUID', ); } if (!utils.isInt(args[0])) { throw utils.patchError( new TypeError(`Failed to execute 'canonicalUUID' on 'BluetoothUUID': Value is not of type 'unsigned long'.`), 'canonicalUUID', ); } return getCanonicalUUID(args[0]); }, }, ); utils.mockWithProxy( window.BluetoothUUID, 'getCharacteristic', _Object.create, { configurable: true, enumerable: true, writable: true, }, { get(target, property, receiver) { if (property === 'name') { return 'getCharacteristic'; } if (property === 'length') { return 1; } return _Reflect.get(target, property, receiver); }, apply(target, thisArg, args) { if (args.length === 0) { throw utils.patchError( new TypeError(`Failed to execute 'getCharacteristic' on 'BluetoothUUID': 1 argument required, but only 0 present.`), 'getCharacteristic', ); } if ('number' === typeof args[0]) { return getCanonicalUUID(args[0]); } if ('string' === typeof args[0]) { // If it is a UUID string, construct a new string directly and return it. if (utils.isUUID(args[0])) { return '' + args[0]; } // In the known Bluetooth Characteristic names: const alias = bluetoothCharacteristics[args[0]]; if (alias) { return getCanonicalUUID(alias); } } throw utils.patchError( new TypeError(`Failed to execute 'getCharacteristic' on 'BluetoothUUID': Invalid Characteristic name: '${args[0]}'. It must be a valid UUID alias (e.g. 0x1234), UUID (lowercase hex characters e.g. '00001234-0000-1000-8000-00805f9b34fb'), or recognized standard name from https://www.bluetooth.com/specifications/gatt/characteristics e.g. 'aerobic_heart_rate_lower_limit'.`), 'getCharacteristic', ); }, }, ); utils.mockWithProxy( window.BluetoothUUID, 'getDescriptor', _Object.create, { configurable: true, enumerable: true, writable: true, }, { get(target, property, receiver) { if (property === 'name') { return 'getDescriptor'; } if (property === 'length') { return 1; } return _Reflect.get(target, property, receiver); }, apply(target, thisArg, args) { if (args.length === 0) { throw utils.patchError( new TypeError(`Failed to execute 'getDescriptor' on 'BluetoothUUID': 1 argument required, but only 0 present.`), 'getDescriptor', ); } if ('number' === typeof args[0]) { return getCanonicalUUID(args[0]); } if ('string' === typeof args[0]) { // If it is a UUID string, construct a new string directly and return it. if (utils.isUUID(args[0])) { return '' + args[0]; } // In the known Bluetooth Descriptors names: const alias = bluetoothDescriptors[args[0]]; if (alias) { return getCanonicalUUID(alias); } } throw utils.patchError( new TypeError(`Failed to execute 'getDescriptor' on 'BluetoothUUID': Invalid Descriptor name: '${args[0]}'. It must be a valid UUID alias (e.g. 0x1234), UUID (lowercase hex characters e.g. '00001234-0000-1000-8000-00805f9b34fb'), or recognized standard name from https://www.bluetooth.com/specifications/gatt/descriptors e.g. 'gatt.characteristic_presentation_format'.`), 'getDescriptor', ); }, }, ); utils.mockWithProxy( window.BluetoothUUID, 'getService', _Object.create, { configurable: true, enumerable: true, writable: true, }, { get(target, property, receiver) { if (property === 'name') { return 'getService'; } if (property === 'length') { return 1; } return _Reflect.get(target, property, receiver); }, apply(target, thisArg, args) { if (args.length === 0) { throw utils.patchError( new TypeError(`Failed to execute 'getService' on 'BluetoothUUID': 1 argument required, but only 0 present.`), 'getService', ); } if ('number' === typeof args[0]) { return getCanonicalUUID(args[0]); } if ('string' === typeof args[0]) { // If it is a UUID string, construct a new string directly and return it. if (utils.isUUID(args[0])) { return '' + args[0]; } // In the known Bluetooth Services names: const alias = bluetoothServices[args[0]]; if (alias) { return getCanonicalUUID(alias); } } throw utils.patchError( new TypeError(`Failed to execute 'getService' on 'BluetoothUUID': Invalid Service name: '${args[0]}'. It must be a valid UUID alias (e.g. 0x1234), UUID (lowercase hex characters e.g. '00001234-0000-1000-8000-00805f9b34fb'), or recognized standard name from https://www.bluetooth.com/specifications/gatt/services e.g. 'alert_notification'.`), 'getService', ); }, }, ); // ============================================================================================== // Bluetooth // Bluetooth.prototype.getAvailability // Bluetooth.prototype.requestDevice // Bluetooth.prototype.[Symbol.toStringTag] utils.mockWithProxy( window.Bluetooth.prototype, 'getAvailability', _Object.create, { configurable: true, enumerable: true, writable: true, }, { get(target, property, receiver) { if (property === 'name') { return 'getAvailability'; } if (property === 'length') { return 0; } return _Reflect.get(target, property, receiver); }, apply(target, thisArg, args) { if (thisArg === window.Bluetooth.prototype) { // Want to call it directly from window.Bluetooth.prototype.getAvailability()? No way! return Promise.reject(utils.patchError( new TypeError(`Failed to execute 'getAvailability' on 'Bluetooth': Illegal invocation`), 'getAvailability', )); } return Promise.resolve(true); }, }, ); utils.mockWithProxy( window.Bluetooth.prototype, 'requestDevice', _Object.create, { configurable: true, enumerable: true, writable: true, }, { get(target, property, receiver) { if (property === 'name') { return 'requestDevice'; } if (property === 'length') { return 0; } return _Reflect.get(target, property, receiver); }, apply(target, thisArg, args) { return new utils.cache.Promise((resolve, reject) => { if (thisArg === window.Bluetooth.prototype) { // Want to call it directly from window.Bluetooth.prototype.requestDevice()? No way! return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': Illegal invocation`), 'requestDevice', )); } // https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice // * filters[]: An array of BluetoothScanFilters. This filter consists of an array of BluetoothServiceUUIDs, a name parameter, and a namePrefix parameter. // * optionalServices[]: An array of BluetoothServiceUUIDs. // * acceptAllDevices: A boolean value indicating that the requesting script can accept all Bluetooth devices. The default is false. if (args.length === 0) { return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': Either 'filters' should be present or 'acceptAllDevices' should be true, but not both.`), 'requestDevice', )); } const { filters, optionalServices, acceptAllDevices, } = args[0]; if ('undefined' !== typeof filters) { // After experimenting, the basic data type throws this exception. if ( filters === null || 'number' === typeof filters || 'boolean' === typeof filters || 'string' === typeof filters || 'symbol' === typeof filters || 'bigint' === typeof filters ) { return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': Failed to read the 'filters' property from 'RequestDeviceOptions': The provided value cannot be converted to a sequence.`), 'requestDevice', )); } if ('function' !== typeof filters[Symbol.iterator]) { return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': Failed to read the 'filters' property from 'RequestDeviceOptions': The object must have a callable @@iterator property.`), 'requestDevice', )); } if (!utils.isSequence(filters)) { return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': Failed to read the 'filters' property from 'RequestDeviceOptions': The provided value cannot be converted to a sequence.`), 'requestDevice', )); } } if ( // filters and acceptAllDevices cannot have values at the same time. (('undefined' === typeof filters) && !acceptAllDevices) // !! Here we can't check filters.length. // According to experiments, filters is considered by chrome to have a value if it is an empty array. || (('undefined' !== typeof filters) /* && filters.length */ && acceptAllDevices) ) { return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': Either 'filters' should be present or 'acceptAllDevices' should be true, but not both.`), 'requestDevice', )); } if (!filters.length) { return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': 'filters' member must be non-empty to find any devices.`), 'requestDevice', )); } for (const filter of filters) { // The filter type can only be: services, name, namePrefix const filterNames = _Object.keys(filter); if ( !utils.intersectionSet( filterNames, ['services', 'name', 'namePrefix'], ).size ) { return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': A filter must restrict the devices in some way.`), 'requestDevice', )); } for (const key in filter) { const value = filter[key]; if ( key === 'services' && 'undefined' !== typeof value ) { const serviceValues = value; if ( serviceValues === null || 'number' === typeof serviceValues || 'boolean' === typeof serviceValues || 'string' === typeof serviceValues || 'symbol' === typeof serviceValues || 'bigint' === typeof serviceValues ) { return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': Failed to read the 'filters' property from 'RequestDeviceOptions': Failed to read the 'services' property from 'BluetoothLEScanFilterInit': The provided value cannot be converted to a sequence.`), 'requestDevice', )); } if ('function' !== typeof serviceValues[Symbol.iterator]) { return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': Failed to read the 'filters' property from 'RequestDeviceOptions': Failed to read the 'services' property from 'BluetoothLEScanFilterInit': The object must have a callable @@iterator property.`), 'requestDevice', )); } if (!utils.isSequence(serviceValues)) { return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': Failed to read the 'filters' property from 'RequestDeviceOptions': Failed to read the 'services' property from 'BluetoothLEScanFilterInit': The provided value cannot be converted to a sequence.`), 'requestDevice', )); } if (!serviceValues.length) { return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': 'services', if present, must contain at least one service.`), 'requestDevice', )); } // Check if each item of serviceValues is legal for (const serviceValue of serviceValues) { if ('symbol' === typeof serviceValue) { return reject(utils.patchError( new TypeError(`Cannot convert a Symbol value to a string`), 'requestDevice', )); } if ('number' === typeof serviceValue) { continue; } if ('string' === typeof serviceValue) { // If it is a UUID string, construct a new string directly and return it. if (utils.isUUID(serviceValue)) { continue; } // In the known Bluetooth Services names: const alias = bluetoothServices[serviceValue]; if (alias) { continue; } } let invalidServiceName; if (serviceValue === null) { invalidServiceName = 'null'; } else if (serviceValue === undefined) { invalidServiceName = 'undefined'; } else { invalidServiceName = serviceValue.toString(); } return reject(utils.patchError( new TypeError(`Failed to execute 'requestDevice' on 'Bluetooth': Invalid Service name: '${invalidServiceName}'. It must be a valid UUID alias (e.g. 0x1234), UUID (lowercase hex characters e.g. '00001234-0000-1000-8000-00805f9b34fb'), or recognized standard name from https://www.bluetooth.com/specifications/gatt/services e.g. 'alert_notification'.`), 'requestDevice', )); } } } } // All checks are done and we need to wait a few seconds to simulate user rejection. const sleepMs = utils.random(1500, 5000); utils.sleep(sleepMs).then(() => { reject(utils.patchError( new DOMException(`User cancelled the requestDevice() chooser.`), 'requestDevice', )); }); }); }, }, ); // We have to implement the three methods inherited from EventTarget :( const eventTarget = new EventTarget(); // noinspection JSUndefinedPropertyAssignment fakeBluetoothInstance.addEventListener = eventTarget.addEventListener.bind(eventTarget); // noinspection JSUndefinedPropertyAssignment fakeBluetoothInstance.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget); // noinspection JSUndefinedPropertyAssignment fakeBluetoothInstance.removeEventListener = eventTarget.removeEventListener.bind(eventTarget); const eventTargetFuncNames = ['addEventListener', 'dispatchEvent', 'removeEventListener']; utils.mockGetterWithProxy( Navigator.prototype, 'bluetooth', _Object.create, { configurable: true, enumerable: true, }, { apply(target, thisArg, args) { return new Proxy( fakeBluetoothInstance, { getOwnPropertyDescriptor: (target, propertyKey) => { if (eventTargetFuncNames.includes(propertyKey)) { return undefined; } return _Reflect.getOwnPropertyDescriptor(target, propertyKey); }, ownKeys: (target) => { let result = _Reflect.ownKeys(target); result = Array.from( utils.differenceABSet(result, eventTargetFuncNames), ); return result; }, }, ); }, }, ); navigatorBluetoothHasSet = true; // ============================================================================================== // BluetoothDevice // BluetoothDevice.prototype.get gatt // BluetoothDevice.prototype.get id // BluetoothDevice.prototype.get name // BluetoothDevice.prototype.get ongattserverdisconnected // BluetoothDevice.prototype.set ongattserverdisconnected // BluetoothDevice.prototype.[Symbol.toStringTag] // Several other classes are similar and we handle them in a unified way. // Covenant: // get default length is 0 // set default length is 1 // value default length is 0 /* // dump props: const classes = [ 'BluetoothDevice', 'BluetoothRemoteGATTCharacteristic', 'BluetoothRemoteGATTDescriptor', 'BluetoothRemoteGATTServer', 'BluetoothRemoteGATTService', ]; const map = []; for (const cls of classes) { const def = { name: cls, props: [], }; map.push(def); const props = Object.getOwnPropertyDescriptors(window[cls].prototype); for (const prop in props) { if (prop === 'constructor') { continue; } const desc = props[prop]; const propDef = { name: prop, descriptor: { configurable: desc.configurable, writable: desc.writable, enumerable: desc.enumerable, }, visit: {}, }; if (desc.get) { propDef.visit.get = {length: desc.get.length, name: desc.get.name}; } if (desc.set) { propDef.visit.set = {length: desc.set.length, name: desc.set.name}; } if (desc.value) { propDef.visit.value = {length: desc.value.length, name: desc.value.name}; } def.props.push(propDef); } } console.log(JSON.stringify(map, null, 4)); */ const map = [ { 'name': 'BluetoothDevice', 'props': [ { 'name': 'id', 'descriptor': { 'configurable': true, 'enumerable': true, }, 'visit': { 'get': { 'length': 0, 'name': 'get id', }, }, }, { 'name': 'name', 'descriptor': { 'configurable': true, 'enumerable': true, }, 'visit': { 'get': { 'length': 0, 'name': 'get name', }, }, }, { 'name': 'gatt', 'descriptor': { 'configurable': true, 'enumerable': true, }, 'visit': { 'get': { 'length': 0, 'name': 'get gatt', }, }, }, { 'name': 'ongattserverdisconnected', 'descriptor': { 'configurable': true, 'enumerable': true, }, 'visit': { 'get': { 'length': 0, 'name': 'get ongattserverdisconnected', }, 'set': { 'length': 1, 'name': 'set ongattserverdisconnected', }, }, }, ], }, { 'name': 'BluetoothRemoteGATTCharacteristic', 'props': [ { 'name': 'service', 'descriptor': { 'configurable': true, 'enumerable': true, }, 'visit': { 'get': { 'length': 0, 'name': 'get service', }, }, }, { 'name': 'uuid', 'descriptor': { 'configurable': true, 'enumerable': true, }, 'visit': { 'get': { 'length': 0, 'name': 'get uuid', }, }, }, { 'name': 'properties', 'descriptor': { 'configurable': true, 'enumerable': true, }, 'visit': { 'get': { 'length': 0, 'name': 'get properties', }, }, }, { 'name': 'value', 'descriptor': { 'configurable': true, 'enumerable': true, }, 'visit': { 'get': { 'length': 0, 'name': 'get value', }, }, }, { 'name': 'oncharacteristicvaluechanged', 'descriptor': { 'configurable': true, 'enumerable': true, }, 'visit': { 'get': { 'length': 0, 'name': 'get oncharacteristicvaluechanged', }, 'set': { 'length': 1, 'name': 'set oncharacteristicvaluechanged', }, }, }, { 'name': 'getDescriptor', 'descriptor': { 'configurable': true, 'writable': true, 'enumerable': true, }, 'visit': { 'value': { 'length': 1, 'name': 'getDescriptor', }, }, }, { 'name': 'getDescriptors', 'descriptor': { 'configurable': true, 'writable': true, 'enumerable': true, }, 'visit': { 'value': { 'length': 0, 'name': 'getDescriptors', }, }, }, { 'name': 'readValue', 'descriptor': { 'configurable': true, 'writable': true, 'enumerable': true, }, 'visit': { 'value': { 'length': 0, 'name': 'readValue', }, }, }, { 'name': 'startNotifications', 'descriptor': { 'configurable': true, 'writable': true, 'enumerable': true, }, 'visit': { 'value': { 'length': 0, 'name': 'startNotifications', }, }, }, { 'name': 'stopNotifications', 'descriptor': { 'configurable': true, 'writable': true, 'enumerable': true, }, 'visit': { 'value': { 'length': 0, 'name': 'stopNotifications', }, }, }, { 'name': 'writeValue', 'descriptor': { 'configurable': true, 'writable': true, 'enumerable': true, }, 'visit': { 'value': { 'length': 1, 'name': 'writeValue'