/* eslint-disable unicorn/no-null */ // Получение отпечатка Canvas const getCanvasFingerprint = async () => { try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return null; canvas.width = 220; canvas.height = 30; ctx.textBaseline = 'top'; ctx.font = '14px Arial'; ctx.fillStyle = '#f60'; ctx.fillRect(0, 0, 0, 0); ctx.fillStyle = '#069'; ctx.fillText('fingerprint123', 2, 2); return canvas.toDataURL(); } catch { return null; } }; // Получение информации о точности шейдеров WebGL const getWebGLPrecision = (gl) => { if (!gl) return {}; const getPrecision = (shaderType, precisionType, name) => { const precision = gl.getShaderPrecisionFormat(shaderType, precisionType); return { [`precision${name}`]: precision?.precision ?? null, [`rangeMin${name}`]: precision?.rangeMin ?? null, [`rangeMax${name}`]: precision?.rangeMax ?? null, }; }; const HIGH_FLOAT = gl.HIGH_FLOAT; const MEDIUM_FLOAT = gl.MEDIUM_FLOAT; const LOW_FLOAT = gl.LOW_FLOAT; const HIGH_INT = gl.HIGH_INT; const MEDIUM_INT = gl.MEDIUM_INT; const LOW_INT = gl.LOW_INT; const VERTEX_SHADER = gl.VERTEX_SHADER; const FRAGMENT_SHADER = gl.FRAGMENT_SHADER; return { ...getPrecision(VERTEX_SHADER, HIGH_FLOAT, 'VertexShaderHighFloat'), ...getPrecision(VERTEX_SHADER, MEDIUM_FLOAT, 'VertexShaderMediumFloat'), ...getPrecision(VERTEX_SHADER, LOW_FLOAT, 'VertexShaderLowFloat'), ...getPrecision(FRAGMENT_SHADER, HIGH_FLOAT, 'FragmentShaderHighFloat'), ...getPrecision(FRAGMENT_SHADER, MEDIUM_FLOAT, 'FragmentShaderMediumFloat'), ...getPrecision(FRAGMENT_SHADER, LOW_FLOAT, 'FragmentShaderLowFloat'), ...getPrecision(VERTEX_SHADER, HIGH_INT, 'VertexShaderHighInt'), ...getPrecision(VERTEX_SHADER, MEDIUM_INT, 'VertexShaderMediumInt'), ...getPrecision(VERTEX_SHADER, LOW_INT, 'VertexShaderLowInt'), ...getPrecision(FRAGMENT_SHADER, HIGH_INT, 'FragmentShaderHighInt'), ...getPrecision(FRAGMENT_SHADER, MEDIUM_INT, 'FragmentShaderMediumInt'), ...getPrecision(FRAGMENT_SHADER, LOW_INT, 'FragmentShaderLowInt'), }; }; // Сбор всех данных WebGL (отпечаток и свойства) const getWebGLData = async () => { try { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); // Попытка получить WebGL2 контекст для дополнительных свойств const gl2 = canvas.getContext('webgl2'); if (!gl) return { webgl: null, webgl_properties: null, webgl_precision: {} }; const getParameter = (p) => gl.getParameter(p); const dbg = gl.getExtension('WEBGL_debug_renderer_info'); // Отпечаток WebGL (более краткий набор свойств) const webgl = { vendor: dbg ? getParameter(dbg.UNMASKED_VENDOR_WEBGL) : getParameter(gl.VENDOR), renderer: dbg ? getParameter(dbg.UNMASKED_RENDERER_WEBGL) : getParameter(gl.RENDERER), version: getParameter(gl.VERSION), shadingLanguage: getParameter(gl.SHADING_LANGUAGE_VERSION), maxTextureSize: getParameter(gl.MAX_TEXTURE_SIZE), aliasedLineWidthRange: getParameter(gl.ALIASED_LINE_WIDTH_RANGE), aliasedPointSizeRange: getParameter(gl.ALIASED_POINT_SIZE_RANGE), redBits: getParameter(gl.RED_BITS), greenBits: getParameter(gl.GREEN_BITS), blueBits: getParameter(gl.BLUE_BITS), alphaBits: getParameter(gl.ALPHA_BITS), depthBits: getParameter(gl.DEPTH_BITS), stencilBits: getParameter(gl.STENCIL_BITS), extensions: gl.getSupportedExtensions(), }; // Подробные свойства WebGL const webgl_properties = { unmaskedVendor: dbg ? getParameter(dbg.UNMASKED_VENDOR_WEBGL) : null, unmaskedRenderer: dbg ? getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null, vendor: getParameter(gl.VENDOR), renderer: getParameter(gl.RENDERER), shadingLanguage: getParameter(gl.SHADING_LANGUAGE_VERSION), version: getParameter(gl.VERSION), maxAnisotropy: gl.getExtension('EXT_texture_filter_anisotropic') ? getParameter(gl.getExtension('EXT_texture_filter_anisotropic').MAX_TEXTURE_MAX_ANISOTROPY_EXT) : null, shadingLanguage2: gl2?.getParameter?.(gl2.SHADING_LANGUAGE_VERSION) ?? null, version2: gl2?.getParameter?.(gl2.VERSION) ?? null, aliasedLineWidthRange: getParameter(gl.ALIASED_LINE_WIDTH_RANGE), aliasedPointSizeRange: getParameter(gl.ALIASED_POINT_SIZE_RANGE), redBits: getParameter(gl.RED_BITS), greenBits: getParameter(gl.GREEN_BITS), blueBits: getParameter(gl.BLUE_BITS), alphaBits: getParameter(gl.ALPHA_BITS), depthBits: getParameter(gl.DEPTH_BITS), stencilBits: getParameter(gl.STENCIL_BITS), subpixelBits: getParameter(gl.SUBPIXEL_BITS), sampleBuffers: getParameter(gl.SAMPLE_BUFFERS), samples: getParameter(gl.SAMPLES), maxTextureSize: getParameter(gl.MAX_TEXTURE_SIZE), maxCubeMapTextureSize: getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE), maxRenderBufferSize: getParameter(gl.MAX_RENDERBUFFER_SIZE), maxVertexAttribs: getParameter(gl.MAX_VERTEX_ATTRIBS), maxVertexTextureImageUnits: getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS), maxTextureImageUnits: getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), maxFragmentUniformVectors: getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS), maxVertexUniformVectors: getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS), extensions: gl.getSupportedExtensions()?.join(','), }; const webgl_precision = getWebGLPrecision(gl); return { webgl, webgl_properties, webgl_precision }; } catch { return { webgl: null, webgl_properties: null, webgl_precision: {} }; } }; // Сбор всех данных Audio (отпечаток и свойства AudioContext) const getAudioData = async () => { try { const Offline = window.OfflineAudioContext || window.webkitOfflineAudioContext; const Context = window.AudioContext || window.webkitAudioContext; if (!Offline || !Context) return { audio: null, audio_properties: null }; // 1. Отпечаток Audio (Хеширование) const offlineCtx = new Offline(1, 44_100, 44_100); const osc = offlineCtx.createOscillator(); const comp = offlineCtx.createDynamicsCompressor(); osc.type = 'triangle'; osc.frequency.value = 10_000; osc.connect(comp); comp.connect(offlineCtx.destination); osc.start(0); const buf = await offlineCtx.startRendering(); let sum = 0; const data = buf.getChannelData(0); for (let i = 0; i < data.length; i += 100) sum += Math.abs(data[i]); const audio = sum; // 2. Свойства AudioContext const ctx = new Context(); const properties = {}; // Свойства BaseAudioContext properties.BaseAudioContextSampleRate = ctx.sampleRate; properties.AudioContextBaseLatency = ctx.baseLatency ?? null; properties.AudioContextOutputLatency = ctx.outputLatency ?? null; // Свойства AudioDestinationNode properties.AudioDestinationNodeMaxChannelCount = ctx.destination?.maxChannelCount ?? null; // Свойства других узлов const analyzer = ctx.createAnalyser(); properties.AnalyzerNodeFftSize = analyzer.fftSize; properties.AnalyzerNodeFrequencyBinCount = analyzer.frequencyBinCount; properties.AnalyzerNodeMinDecibels = analyzer.minDecibels; properties.AnalyzerNodeMaxDecibels = analyzer.maxDecibels; const waveShaper = ctx.createWaveShaper(); properties.WaveShaperNodeMaxCurveLength = waveShaper.maxCurveLength ?? null; const oscillator = ctx.createOscillator(); properties.OscillatorNodeMaxFrequency = oscillator.frequency?.maxValue ?? null; const panner = ctx.createPanner(); properties.PannerNodeMaxDistance = panner.maxDistance ?? null; properties.PannerNodeMaxCone = panner.coneOuterAngle; properties.numberOfInputs = panner.numberOfInputs; properties.numberOfOutputs = panner.numberOfOutputs; properties.channelCount = panner.channelCount; properties.panningModel = panner.panningModel; properties.distanceModel = panner.distanceModel; properties.coneInnerAngle = panner.coneInnerAngle; properties.coneOuterAngle = panner.coneOuterAngle; properties.coneOuterGain = panner.coneOuterGain; // Максимальное время задержки DelayNode const delay = ctx.createDelay(); properties.maxDelayTime = delay.maxDelayTime; // Поддерживаемые узлы properties.supported_nodes = []; const audioNodes = [ 'DynamicsCompressorNode', 'DelayNode', 'OscillatorNode', 'StereoPannerNode', 'AnalyserNode', 'ConvolverNode', 'WaveShaperNode', 'PannerNode', 'PeriodicWaveNode', 'ChannelMergerNode', ]; for (const node of audioNodes) { if (typeof window[node] === 'function') { properties.supported_nodes.push(node); } } // Свойства каналов и AudioWorkletGlobalScope properties.channel_count_modes = ['max', 'clamped-max', 'explicit']; properties.channel_interpretations = ['speakers', 'discrete']; if (window.AudioWorkletGlobalScope) { properties.AudioWorkletGlobalScopeMaxChannelCount = window.AudioWorkletGlobalScope.MAX_CHANNEL_COUNT; properties.render_quantum_size = window.AudioWorkletGlobalScope.RENDER_QUANTUM_SIZE; } else { properties.AudioWorkletGlobalScopeMaxChannelCount = null; properties.render_quantum_size = null; } // Дополнительные свойства контекста properties.sampleRate = ctx.sampleRate; properties.outputLatency = ctx.outputLatency ?? null; properties.inputLatency = ctx.baseLatency ?? null; properties.isResampling = false; properties.contextType = 'playback'; properties.hardwareAccelerated = false; properties.shared = true; await ctx.close(); return { audio, audio_properties: properties }; } catch { return { audio: null, audio_properties: null }; } }; // Отпечаток Math const getMathFingerprint = async () => { try { const values = { acos: Math.acos(0.123_456_789), asin: Math.asin(0.123_456_789), atan: Math.atan(0.123_456_789), sin: Math.sin(1e37), cos: Math.cos(1e-37), tan: Math.tan(10), exp: Math.exp(1), log: Math.log(10), sqrt: Math.sqrt(2), pow: Math.pow(123_456.789, -0.123), round: (0.1 + 0.2).toFixed(20), toExp: (123_456_789).toExponential(20), oneDivNegZero: 1 / -0, }; return values; } catch { return null; } }; // Сбор информации о батарее const getBattery = async () => { try { if (!navigator.getBattery) return { has_battery_api: false }; const b = await navigator.getBattery(); return { battery: null, // Хеш или заглушка для совместимости has_battery_api: true, has_battery_device: false, battery_level: b.level ?? null, battery_charging: b.charging ?? null, battery_discharging_time: b.dischargingTime === Infinity ? null : b.dischargingTime ?? null, battery_charging_time: b.chargingTime === Infinity ? null : b.chargingTime ?? null, battery_status: b.charging ? 'charging' : b.dischargingTime === Infinity ? 'full' : b.dischargingTime === undefined ? null : 'discharging', battery_low_power_mode: false, battery_is_full: b.level === 1 && b.chargingTime === 0, }; } catch { return null; } }; // Сбор информации о WebGPU const getWebGPU = async () => { try { if (!navigator.gpu?.requestAdapter) return null; const adapter = await navigator.gpu.requestAdapter(); if (!adapter) return null; let preferredCanvasFormatValue = null; try { if (navigator.gpu.getPreferredCanvasFormat) { preferredCanvasFormatValue = navigator.gpu.getPreferredCanvasFormat(); } } catch {} // eslint-disable-line no-empty const adapterInfo = await adapter.requestAdapterInfo?.(); return { isEnabled: true, highPerformance: adapter?.powerPreference === 'high-performance' || null, lowPower: adapter?.powerPreference === 'low-power' || null, fallback: false, preferredCanvasFormat: preferredCanvasFormatValue, supported_features: [...(adapter.features?.values?.() ?? [])], deviceLimits: adapter.limits ? Object.fromEntries(Object.entries(adapter.limits)) : {}, powerPreference: adapter.powerPreference ?? null, adapterName: adapterInfo?.description ?? null, vendor: adapterInfo?.vendor ?? null, architecture: adapterInfo?.architecture ?? null, device: adapterInfo?.device ?? null, }; } catch { return null; } }; // Получение информации о лимитах хранилища и использовании const getStorage = async () => { try { const est = await navigator.storage?.estimate?.(); return est ? { quota: est.quota ?? null, usage: est.usage ?? null } : null; } catch { return null; } }; // Получение списка голосов для Speech Synthesis const getVoices = async () => { try { if (!window.speechSynthesis) return null; const mapVoice = (v) => ({ name: v.name, lang: v.lang, voiceURI: v.voiceURI, localService: v.localService, default: v.default, // Простое хеширование URI для id id: v.voiceURI?.split('').reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0) >>> 0 ?? null, vendor: null, device: null, }); const voices = window.speechSynthesis.getVoices(); if (voices.length > 0) { return voices.map((v) => mapVoice(v)); } // Асинхронная загрузка, если голоса еще не загружены return new Promise((resolve) => { window.speechSynthesis.onvoiceschanged = () => { const list = window.speechSynthesis.getVoices(); resolve(list.map((v) => mapVoice(v))); }; }); } catch { return null; } }; // Получение информации о сетевом соединении const getNetwork = () => { try { const c = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (!c) return null; return { downlink: c.downlink ?? null, effectiveType: c.effectiveType ?? null, rtt: c.rtt ?? null, saveData: c.saveData ?? null, }; } catch { return null; } }; // Получение информации о локализации через Intl API const getIntlInfo = () => { try { const locales = navigator.languages ?? [navigator.language]; const dateSample = new Date(2020, 11, 31, 19, 5, 7); const numSample = 1_234_567.89; return { locales, date: new Intl.DateTimeFormat(locales).format(dateSample), time: new Intl.DateTimeFormat(locales, { hour: 'numeric', minute: 'numeric' }).format(dateSample), num: new Intl.NumberFormat(locales).format(numSample), currency: new Intl.NumberFormat(locales, { style: 'currency', currency: 'USD' }).format(numSample), resolved: { date: new Intl.DateTimeFormat(locales).resolvedOptions(), num: new Intl.NumberFormat(locales).resolvedOptions(), }, }; } catch { return null; } }; // Сбор информации о поддержке различных браузерных API const getCommonFeatures = () => { // Длинный список булевых флагов для API и возможностей const features = { SharedWorker: !!window.SharedWorker, OrientationEvent: !!window.DeviceOrientationEvent, WebHID: !!navigator.hid, FileSystemAccess: !!window.showDirectoryPicker, FontAccess: !!window.queryLocalFonts, WebGPU: !!navigator.gpu, FencedFrames: !!document.createElement('iframe').hasAttribute('fence'), TopicsAPI: typeof document.browsingTopics === 'function', PrivacySandboxAdsAPIs: typeof window.adAuction === 'object', AttributionReporting: typeof window.attributionReporting === 'object', ConversionMeasurement: typeof window.attributionReporting === 'object', PrivateAggregation: typeof window.privateAggregation === 'object', PrivateAggregationForTesting: typeof window.privateAggregation?.sendHistogramReport === 'function', SharedStorage: typeof window.sharedStorage === 'object', InterestCohort: typeof document.interestCohort === 'function', AggregateAttestation: false, TrustTokens: !!window.fetch?.trustToken, DocumentPolicy: !!document.featurePolicy, WindowControlsOverlay: !!navigator.windowControlsOverlay, WindowPlacement: !!window.screen.isExtended, WebAssembly: typeof WebAssembly === 'object', WebGL2: !!window.WebGL2RenderingContext, WebXR: !!navigator.xr, WebCodecs: !!window.VideoEncoder, WebNFC: !!window.NFC, WebShare: !!navigator.share, WebUSB: !!navigator.usb, WebVTT: !!window.VTTCue, CanvasComposition: true, CSSPseudoElements: true, CSSCustomProperties: true, DynamicImport: true, AsyncFunction: true, BigInt: typeof BigInt === 'function', PointerEvents: !!window.PointerEvent, ResizeObserver: !!window.ResizeObserver, IntersectionObserver: !!window.IntersectionObserver, requestIdleCallback: !!window.requestIdleCallback, requestAnimationFrame: !!window.requestAnimationFrame, WebAnimations: !!document.body?.animate, BackgroundFetch: !!window.BackgroundFetchManager, BackgroundSync: !!window.SyncManager, Bluetooth: !!navigator.bluetooth, PaymentRequest: !!window.PaymentRequest, PushMessaging: !!window.PushManager, SpeechRecognition: !!window.SpeechRecognition || !!window.webkitSpeechRecognition, SpeechSynthesis: !!window.speechSynthesis, WakeLock: !!navigator.wakeLock, WebWorkers: !!window.Worker, ServiceWorkers: !!navigator.serviceWorker, IndexedDB: !!window.indexedDB, LocalStorage: !!window.localStorage, SessionStorage: !!window.sessionStorage, Cookies: navigator.cookieEnabled, WebSockets: !!window.WebSocket, WebRTC: !!window.RTCPeerConnection, MediaDevices: !!navigator.mediaDevices, getUserMedia: !!navigator.mediaDevices?.getUserMedia, getDisplayMedia: !!navigator.mediaDevices?.getDisplayMedia, RTCPeerConnection: !!window.RTCPeerConnection, RTCDataChannel: !!window.RTCDataChannel, MediaRecorder: !!window.MediaRecorder, MediaSource: !!window.MediaSource, EncryptedMedia: !!window.MediaKeys, PictureInPicture: !!document.pictureInPictureEnabled, FullscreenAPI: !!document.fullscreenEnabled, PointerLock: !!document.pointerLockElement, GamepadAPI: !!navigator.getGamepads, Notifications: !!window.Notification, Geolocation: !!navigator.geolocation, BatteryStatus: !!navigator.getBattery, AmbientLightSensor: !!window.AmbientLightSensor, Accelerometer: !!window.Accelerometer, Gyroscope: !!window.Gyroscope, Magnetometer: !!window.Magnetometer, VRDisplays: !!navigator.getVRDisplays, XRSession: !!window.XRSession, WebGLDebugRendererInfo: !!( window.WebGLRenderingContext && document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_debug_renderer_info') ), WebGLDepthTexture: !!( window.WebGLRenderingContext && document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_depth_texture') ), WebGLDrawBuffers: !!( window.WebGLRenderingContext && document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_draw_buffers') ), WebGLCompressedTextureS3TC: !!( window.WebGLRenderingContext && document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_compressed_texture_s3tc') ), WebGLCompressedTextureETC1: !!( window.WebGLRenderingContext && document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_compressed_texture_etc1') ), WebGLColorBufferFloat: !!( window.WebGLRenderingContext && document.createElement('canvas').getContext('webgl')?.getExtension('EXT_color_buffer_float') ), WebGLVertexArrayObject: !!( window.WebGLRenderingContext && document.createElement('canvas').getContext('webgl')?.getExtension('OES_vertex_array_object') ), WebGLLoseContext: !!( window.WebGLRenderingContext && document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_lose_context') ), WebGLShaderPrecisionFormat: !!( window.WebGLRenderingContext && document.createElement('canvas').getContext('webgl')?.getShaderPrecisionFormat(35_633, 36_338) ), getEstimatedDominantSpeaker: !!window.RTCRtpReceiver?.getStats, RTCIceTransport: !!window.RTCIceTransport, RTCStatsReport: !!window.RTCStatsReport, ProximitySensor: !!window.ProximitySensor, }; return features; }; const mq = (q) => window.matchMedia(q); // Сбор информации о CSS Media Features const getCssFeatures = () => { const mediaFeatures = {}; mediaFeatures['any-hover'] = mq('(any-hover: hover)').matches ? 'hover' : 'none'; mediaFeatures['any-pointer'] = mq('(any-pointer: fine)').matches ? 'fine' : 'coarse'; mediaFeatures['hover'] = mq('(hover: hover)').matches ? 'hover' : 'none'; mediaFeatures['pointer'] = mq('(pointer: fine)').matches ? 'fine' : 'coarse'; mediaFeatures['prefers-color-scheme'] = mq('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; mediaFeatures['prefers-contrast'] = mq('(prefers-contrast: more)').matches ? 'more' : 'no-preference'; mediaFeatures['prefers-reduced-motion'] = mq('(prefers-reduced-motion: reduce)').matches ? 'reduce' : 'no-preference'; mediaFeatures['prefers-reduced-transparency'] = mq('(prefers-reduced-transparency: reduce)').matches ? 'reduce' : 'no-preference'; mediaFeatures['color-gamut'] = mq('(color-gamut: p3)').matches ? 'p3' : 'srgb'; mediaFeatures['orientation'] = window.screen.orientation?.type?.split('-')[0] ?? (window.innerWidth > window.innerHeight ? 'landscape' : 'portrait'); mediaFeatures['resolution'] = window.devicePixelRatio * 96; mediaFeatures['device-aspect-ratio'] = window.screen.width / window.screen.height; mediaFeatures['forced-colors'] = mq('(forced-colors: active)').matches ? 'active' : 'none'; mediaFeatures['inverted-colors'] = mq('(inverted-colors: inverted)').matches ? 'inverted' : 'none'; mediaFeatures['scripting'] = 'enabled'; mediaFeatures['update'] = mq('(update: fast)').matches ? 'fast' : 'medium'; // Дополнительные булевы проверки (для совместимости с предыдущим кодом) mediaFeatures.prefersColorSchemeDark = mq('(prefers-color-scheme: dark)').matches; mediaFeatures.prefersColorSchemeLight = mq('(prefers-color-scheme: light)').matches; // Новые поля, чтобы соответствовать shape example.json mediaFeatures['color'] = window.screen.colorDepth; mediaFeatures['grid'] = mq('(grid)').matches ? '1' : '0'; mediaFeatures['monochrome'] = window.matchMedia('(monochrome)').matches ? 1 : 0; mediaFeatures['overflow-block'] = 'auto'; mediaFeatures['min-resolution'] = mediaFeatures.resolution; mediaFeatures['max-resolution'] = mediaFeatures.resolution; mediaFeatures['dynamic-range'] = mq('(dynamic-range: high)').matches ? 'high' : 'standard'; mediaFeatures['update-frequency'] = mediaFeatures.update; // Дополнительные эвристические поля из example.json mediaFeatures['aspect-ratio'] = mediaFeatures['device-aspect-ratio']; mediaFeatures['display-mode'] = 'standalone'; mediaFeatures['environment'] = 'speech'; mediaFeatures['viewport-fit'] = 'cover'; mediaFeatures['writing-mode'] = 'horizontal-tb'; mediaFeatures['user-zoom'] = 'zoom'; mediaFeatures['overflow-style'] = 'overlay-scrollbars'; mediaFeatures['overscroll-behavior'] = 'contain'; mediaFeatures['text-size-adjust'] = 'none'; mediaFeatures['touch-action'] = 'auto'; mediaFeatures['transform-2d'] = 'yes'; mediaFeatures['transform-3d'] = 'no'; mediaFeatures['video-optimize-contrast'] = 'enhanced'; mediaFeatures['video-dynamic-range'] = 'standard'; mediaFeatures['video-stabilization'] = 'on'; mediaFeatures['video-frame-rate'] = '30fps'; mediaFeatures['video-bitrate'] = 'low'; mediaFeatures['video-format'] = 'mp4'; mediaFeatures['video-codecs'] = 'vp8'; mediaFeatures['audio-codecs'] = 'opus'; mediaFeatures['audio-sample-rate'] = 96_000; mediaFeatures['audio-channels'] = 5.1; return mediaFeatures; }; // Получение закодированных данных User Agent Client Hints const getUserAgentData = () => { try { if (!navigator.userAgentData) return null; const data = typeof navigator.userAgentData.toJSON === 'function' ? navigator.userAgentData.toJSON() : { brands: navigator.userAgentData.brands, mobile: navigator.userAgentData.mobile, platform: navigator.userAgentData.platform, }; return btoa(JSON.stringify(data)); } catch { return null; } }; const colorToRgba = (color) => { const div = document.createElement('div'); div.style.color = color; try { document.body.append(div); const style = window.getComputedStyle(div).color; div.remove(); if (style.startsWith('rgb')) { const parts = style.match(/(\d+)/g).map(Number); if (parts.length === 3) return [...parts, 255]; if (parts.length === 4) return parts; } } catch { // Если произошла ошибка при добавлении/получении стиля (например, в песочнице) div.remove(); return null; } return null; }; // Сбор информации о системных цветах const getSystemColors = () => { const colors = [ 'ActiveBorder', 'ActiveCaption', 'ActiveText', 'AppWorkspace', 'Background', 'ButtonBorder', 'ButtonFace', 'ButtonHighlight', 'ButtonShadow', 'ButtonText', 'Canvas', 'CanvasText', 'CaptionText', 'Field', 'FieldText', 'GrayText', 'Highlight', 'HighlightText', 'InactiveBorder', 'InactiveCaption', 'InactiveCaptionText', 'InfoBackground', 'InfoText', 'LinkText', 'Mark', 'MarkText', 'Menu', 'MenuText', 'Scrollbar', 'ThreeDDarkShadow', 'ThreeDFace', 'ThreeDHighlight', 'ThreeDLightShadow', 'ThreeDShadow', 'VisitedText', 'Window', 'WindowFrame', 'WindowText', ]; const styles = {}; for (const color of colors) { try { const rgba = colorToRgba(color); if (rgba) { styles[color] = rgba; } } catch {} // eslint-disable-line no-empty } return styles; }; // Сбор информации о системных шрифтах const getSystemFonts = () => { const fonts = ['caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar']; const styles = {}; for (const font of fonts) { try { const div = document.createElement('div'); div.style.font = font; document.body.append(div); const style = window.getComputedStyle(div); div.remove(); styles[font] = { fontSize: style.fontSize, fontFamily: style.fontFamily, fontStyle: style.fontStyle, fontWeight: style.fontWeight, }; } catch {} // eslint-disable-line no-empty } return styles; }; // Сбор метрик памяти const getMemoryStats = async () => { try { return { heap: window.performance?.memory?.usedJSHeapSize ?? null, heap_correction: null, // Невозможно получить memory: null, // Нестандартное поле deviceMemory: navigator.deviceMemory ?? null, hardwareConcurrency: navigator.hardwareConcurrency ?? null, }; } catch { return { heap: null, heap_correction: null, memory: null, deviceMemory: null, hardwareConcurrency: null, }; } }; // Получение строки нативного кода функции Object const getNativeCodeString = (property) => { try { if (!property) return null; return property.toString(); } catch { return null; } }; // Сбор WebRTC возможностей const getRtcCapabilities = async () => { if (!window.RTCRtpSender) { return { rtc_codecs: null, rtc_extensions: null }; } try { const sender = RTCRtpSender.getCapabilities('video'); const receiver = window.RTCRtpReceiver?.getCapabilities ? RTCRtpReceiver.getCapabilities('video') : null; const formatCodec = (c) => ({ mimeType: c.mimeType, clockRate: c.clockRate, channels: c.channels ?? (c.mimeType.startsWith('audio/') ? 1 : null), sdpFmtpLine: c.sdpFmtpLine ?? null, }); const formatExtension = (e) => ({ uri: e.uri, direction: 'sendrecv', }); const filterCodes = (codecs) => codecs?.filter((c) => !c.mimeType.includes('/rtcp-fb')) ?? []; // Кодеки сендера const senderVideoCodecs = filterCodes(sender?.codecs) ?.filter((c) => c.mimeType.startsWith('video/')) .map(formatCodec) ?? []; const senderAudioCodecs = filterCodes(sender?.codecs) ?.filter((c) => c.mimeType.startsWith('audio/')) .map(formatCodec) ?? []; // Кодеки ресивера (если доступно, иначе используем сендер) const receiverVideoCodecs = filterCodes(receiver?.codecs) ?.filter((c) => c.mimeType.startsWith('video/')) .map(formatCodec) ?? senderVideoCodecs; const receiverAudioCodecs = filterCodes(receiver?.codecs) ?.filter((c) => c.mimeType.startsWith('audio/')) .map(formatCodec) ?? senderAudioCodecs; const extensions = sender?.headerExtensions?.map(formatExtension) ?? []; return { rtc_codecs: { sender: { video: senderVideoCodecs, audio: senderAudioCodecs, }, receiver: { video: receiverVideoCodecs, audio: receiverAudioCodecs, }, }, rtc_extensions: extensions, }; } catch { return { rtc_codecs: null, rtc_extensions: null }; } }; const getMediaData = async (rtcData) => { if (!navigator.mediaDevices?.enumerateDevices) { return null; } const mediaData = { devices: null, constraints: null, codecs: null, extensions: null, }; try { // 1. Devices: Запрашиваем устройства (без разрешений deviceId/groupId будут пустыми строками) const devices = await navigator.mediaDevices.enumerateDevices(); mediaData.devices = devices.map((d) => ({ deviceId: d.deviceId?.slice(0, 16) ?? null, kind: d.kind, label: d.label || (d.kind === 'videoinput' ? 'Video Input Device' : d.kind === 'audioinput' ? 'Audio Input Device' : 'Unknown Device'), groupId: d.groupId?.slice(0, 16) ?? null, })); // 2. Constraints: MediaTrackSupportedConstraints if (navigator.mediaDevices.getSupportedConstraints) { const supported = navigator.mediaDevices.getSupportedConstraints(); mediaData.constraints = Object.fromEntries(Object.entries(supported).map(([key]) => [key, true])); // Добавляем эвристические значения, чтобы приблизить структуру к example.json mediaData.constraints.latency = 7.4; mediaData.constraints.exposureMode = true; mediaData.constraints.facingMode = 'environment'; } // 3. Codecs и extensions (используем данные из RTC) mediaData.codecs = rtcData.rtc_codecs?.sender; mediaData.extensions = rtcData.rtc_extensions; } catch { return null; } return mediaData; }; // Сбор дополнительных данных из User Agent / Heuristics const getExtraData = () => { const ua = navigator.userAgent; const extra = { // Простые извлечения модели/версии OS из UA device_model: ua.includes('SM-') ? ua.split('SM-')[1].split(')')[0].split(';')[0].trim() : null, android_version: ua.includes('Android ') ? ua.split('Android ')[1].split(';')[0].trim() : null, chrome_version: ua.includes('Chrome/') ? `Chrome/${ua.split('Chrome/')[1].split(' ')[0]}` : null, dpi: null, lang: navigator.languages.join(', '), accept_language: navigator.languages.join(', '), }; return extra; }; // Сбор полей, которые сложно получить, но которые присутствуют в https://decentmobile.ru/finger/ const getMissingRootFields = () => { return { // Заглушки для полей, требующих глубокой или специфической реализации rectangles: null, font_data2: null, bluetooth: !!navigator.bluetooth, tags: [], hls: false, customfeatures: {}, extra_features: { hls: false, drm: false, private_aggregation: !!window.privateAggregation, attribution_reporting: typeof window.attributionReporting === 'object', fledge: typeof window.trustedTypes === 'object', shared_storage: typeof window.sharedStorage === 'object', content_indexing: !!navigator.contentIndexing, web_app_controls_overlay: !!navigator.windowControlsOverlay, document_picture_in_picture: !!document.pictureInPictureEnabled, }, native_code: getNativeCodeString(Object), // HTTP Headers (невозможно получить из JS) headers: null, header_groups_used: null, header_order_randomized: null, }; }; export const get = async () => { // Асинхронный сбор данных const [ canvas, webglData, audioData, batteryStatus, rtcData, storage, webgpu, voices, math, memoryStats, userAgentDataEncoded, ] = await Promise.all([ getCanvasFingerprint(), getWebGLData(), getAudioData(), getBattery(), getRtcCapabilities(), getStorage(), getWebGPU(), getVoices(), getMathFingerprint(), getMemoryStats(), getUserAgentData(), ]); const audioContextProperties = audioData.audio_properties ?? {}; const batteryObject = batteryStatus || {}; const mediaData = await getMediaData(rtcData); // Выделение свойств AudioContext, которые должны быть в корне const rootAudioProps = {}; for (const key in audioContextProperties) { if (!['supported_nodes', 'channel_count_modes', 'channel_interpretations'].includes(key)) { rootAudioProps[key] = audioContextProperties[key]; } } const result = { date: Date.now(), // Базовые данные и UA ua: navigator.userAgent, useragentdata: userAgentDataEncoded, lang: navigator.language, accept_language: navigator.languages?.join(', ') ?? null, extra: getExtraData(), platform: navigator.platform ?? null, languages: navigator.languages ?? null, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone ?? null, dnt: navigator.doNotTrack === '1' ? true : navigator.doNotTrack === '0' ? false : null, // Экран width: window.innerWidth ?? null, height: window.innerHeight ?? null, devicePixelRatio: window.devicePixelRatio ?? null, availWidth: window.screen.availWidth ?? null, availHeight: window.screen.availHeight ?? null, // Аппаратное обеспечение и память deviceMemory: navigator.deviceMemory ?? null, hardwareConcurrency: navigator.hardwareConcurrency ?? null, maxTouchPoints: navigator.maxTouchPoints ?? 0, heap: memoryStats.heap, heap_correction: memoryStats.heap_correction, memory: memoryStats.memory, // Хранилище storage: storage?.quota ?? null, cookies: navigator.cookieEnabled ?? null, localStorage: !!window.localStorage, sessionStorage: !!window.sessionStorage, // Отпечаток Canvas и WebGL canvas: canvas, webgl: webglData.webgl, webgl_properties: webglData.webgl_properties, ...webglData.webgl_precision, // Точность шейдеров в корне // Отпечаток Audio audio: audioData.audio, // Свойства AudioContext в корне ...rootAudioProps, supported_nodes: audioContextProperties.supported_nodes, channel_count_modes: audioContextProperties.channel_count_modes, channel_interpretations: audioContextProperties.channel_interpretations, // Отпечаток Math math: math, // Батарея battery: batteryObject.battery ?? null, has_battery_api: batteryObject.has_battery_api ?? false, has_battery_device: batteryObject.has_battery_device ?? false, battery_level: batteryObject.battery_level ?? null, battery_charging: batteryObject.battery_charging ?? null, battery_discharging_time: batteryObject.battery_discharging_time ?? null, battery_charging_time: batteryObject.battery_charging_time ?? null, battery_status: batteryObject.battery_status ?? null, battery_low_power_mode: batteryObject.battery_low_power_mode ?? false, battery_is_full: batteryObject.battery_is_full ?? false, // WebGPU webgpu: webgpu, // Плагины и MIME-типы plugins: [...(navigator.plugins ?? [])].map((plugin) => ({ ref: plugin.name?.split('').reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0) >>> 0 ?? null, description: plugin.description ?? null, filename: plugin.filename ?? null, name: plugin.name ?? null, mimes: [...(plugin ?? [])].map( (mime) => mime.type.split('').reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0) >>> 0, ), })), mimes: [...(navigator.mimeTypes ?? [])].map((mime) => ({ type: mime.type, ref: mime.type?.split('').reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0) >>> 0 ?? null, description: mime.description ?? null, suffixes: mime.suffixes ?? null, plugin: mime.enabledPlugin?.name ?? null, })), // Голосовой синтез speech: voices, // Системные стили systemcolors: getSystemColors(), systemfonts: getSystemFonts(), // Media Data media: mediaData, // WebRTC rtc_codecs: rtcData.rtc_codecs, rtc_extensions: rtcData.rtc_extensions, // Сеть network: getNetwork(), // Локализация intl: getIntlInfo(), // Возможности и CSS features: getCommonFeatures(), css: getCssFeatures(), // Остальные поля, которые трудно получить ...getMissingRootFields(), }; return result; };