diff --git a/index.js b/index.js index 82cab39..1fddfc0 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ /* eslint-disable unicorn/no-null */ +// Получение отпечатка Canvas const getCanvasFingerprint = async () => { try { const canvas = document.createElement('canvas'); @@ -10,7 +11,7 @@ const getCanvasFingerprint = async () => { ctx.textBaseline = 'top'; ctx.font = '14px Arial'; ctx.fillStyle = '#f60'; - ctx.fillRect(0, 0, 50, 30); + ctx.fillRect(0, 0, 0, 0); ctx.fillStyle = '#069'; ctx.fillText('fingerprint123', 2, 2); return canvas.toDataURL(); @@ -19,56 +20,238 @@ const getCanvasFingerprint = async () => { } }; -const getWebGLFingerprint = async () => { +// Получение информации о точности шейдеров 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'); - if (!gl) return null; + 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'); - const props = { - vendor: dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR), - renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER), - version: gl.getParameter(gl.VERSION), - shadingLanguage: gl.getParameter(gl.SHADING_LANGUAGE_VERSION), - maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE), - aliasedLineWidthRange: gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE), - aliasedPointSizeRange: gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE), - redBits: gl.getParameter(gl.RED_BITS), - greenBits: gl.getParameter(gl.GREEN_BITS), - blueBits: gl.getParameter(gl.BLUE_BITS), - alphaBits: gl.getParameter(gl.ALPHA_BITS), - depthBits: gl.getParameter(gl.DEPTH_BITS), - stencilBits: gl.getParameter(gl.STENCIL_BITS), + + // Отпечаток 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(), }; - return props; + + // Подробные свойства 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 null; + return { webgl: null, webgl_properties: null, webgl_precision: {} }; } }; -const getAudioFingerprint = async () => { +// Сбор всех данных Audio (отпечаток и свойства AudioContext) +const getAudioData = async () => { try { const Offline = window.OfflineAudioContext || window.webkitOfflineAudioContext; - if (!Offline) return null; - const ctx = new Offline(1, 44_100, 44_100); - const osc = ctx.createOscillator(); - const comp = ctx.createDynamicsCompressor(); + 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(ctx.destination); + comp.connect(offlineCtx.destination); osc.start(0); - const buf = await ctx.startRendering(); + 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]); - return sum; + 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 null; + return { audio: null, audio_properties: null }; } }; +// Отпечаток Math const getMathFingerprint = async () => { try { const values = { @@ -92,45 +275,80 @@ const getMathFingerprint = async () => { } }; +// Сбор информации о батарее const getBattery = async () => { try { - if (!navigator.getBattery) return null; + if (!navigator.getBattery) return { has_battery_api: false }; const b = await navigator.getBattery(); return { - charging: b.charging, - level: b.level, - chargingTime: b.chargingTime, - dischargingTime: b.dischargingTime, + 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 { - name: adapter.name, - features: [...(adapter.features?.values?.() ?? [])], - limits: adapter.limits ? Object.fromEntries(Object.entries(adapter.limits)) : {}, + 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 { quota: est.quota, usage: est.usage }; + 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; @@ -138,8 +356,13 @@ const getVoices = async () => { const mapVoice = (v) => ({ name: v.name, lang: v.lang, - local: v.localService, + 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(); @@ -147,7 +370,8 @@ const getVoices = async () => { return voices.map((v) => mapVoice(v)); } - return await new Promise((resolve) => { + // Асинхронная загрузка, если голоса еще не загружены + return new Promise((resolve) => { window.speechSynthesis.onvoiceschanged = () => { const list = window.speechSynthesis.getVoices(); resolve(list.map((v) => mapVoice(v))); @@ -158,21 +382,23 @@ const getVoices = async () => { } }; +// Получение информации о сетевом соединении const getNetwork = () => { try { const c = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (!c) return null; return { - downlink: c.downlink, - effectiveType: c.effectiveType, - rtt: c.rtt, - saveData: c.saveData, + 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]; @@ -194,102 +420,673 @@ const getIntlInfo = () => { } }; -const getFeatures = () => ({ - webgl: !!window.WebGLRenderingContext, - workers: !!window.Worker, - wasm: typeof WebAssembly === 'object', - hid: !!navigator.hid, - bluetooth: !!navigator.bluetooth, - gpu: !!navigator.gpu, - share: !!navigator.share, - clipboard: !!navigator.clipboard, - idle: !!navigator.requestIdleCallback, -}); +// Сбор информации о поддержке различных браузерных 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).matches; +const mq = (q) => window.matchMedia(q); -const getCSSFeatures = () => ({ - prefersColorSchemeDark: mq('(prefers-color-scheme: dark)'), - prefersColorSchemeLight: mq('(prefers-color-scheme: light)'), - prefersReducedMotion: mq('(prefers-reduced-motion: reduce)'), - prefersReducedTransparency: mq('(prefers-reduced-transparency: reduce)'), - prefersReducedData: mq('(prefers-reduced-data: reduce)'), - prefersContrastMore: mq('(prefers-contrast: more)'), - prefersContrastLess: mq('(prefers-contrast: less)'), - forcedColorsActive: mq('(forced-colors: active)'), - invertedColorsInverted: mq('(inverted-colors: inverted)'), - hoverHover: mq('(hover: hover)'), - hoverNone: mq('(hover: none)'), - anyHoverHover: mq('(any-hover: hover)'), - anyHoverNone: mq('(any-hover: none)'), - pointerFine: mq('(pointer: fine)'), - pointerCoarse: mq('(pointer: coarse)'), - pointerNone: mq('(pointer: none)'), - anyPointerFine: mq('(any-pointer: fine)'), - anyPointerCoarse: mq('(any-pointer: coarse)'), - anyPointerNone: mq('(any-pointer: none)'), - lightLevelDim: mq('(light-level: dim)'), - lightLevelNormal: mq('(light-level: normal)'), - lightLevelWashed: mq('(light-level: washed)'), -}); +// Сбор информации о CSS Media Features +const getCssFeatures = () => { + const mediaFeatures = {}; -export const get = async () => { + 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 { - date: Date.now(), + // Заглушки для полей, требующих глубокой или специфической реализации + rectangles: null, + font_data2: null, + bluetooth: !!navigator.bluetooth, + tags: [], - // Базовые данные - ua: navigator.userAgent, - platform: navigator.platform ?? null, - lang: navigator.language, - languages: navigator.languages, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - dnt: navigator.doNotTrack === '1', + 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), - // Hardware - width: window.innerWidth, - height: window.innerHeight, - devicePixelRatio: window.devicePixelRatio, - hardwareConcurrency: navigator.hardwareConcurrency, - deviceMemory: navigator.deviceMemory ?? null, - maxTouchPoints: navigator.maxTouchPoints ?? 0, - - // Storage / Cookies - cookies: navigator.cookieEnabled, - localStorage: !!window.localStorage, - sessionStorage: !!window.sessionStorage, - storage: await getStorage(), - - // Fingerprints - canvas: await getCanvasFingerprint(), - webgl: await getWebGLFingerprint(), - audio: await getAudioFingerprint(), - math: await getMathFingerprint(), - - // Power - battery: await getBattery(), - - // Graphics - webgpu: await getWebGPU(), - - // Lists - plugins: [...(navigator.plugins ?? [])].map(({ name, filename, description }) => ({ - name, - filename, - description, - })), - mimes: [...(navigator.mimeTypes ?? [])].map(({ type, suffixes }) => ({ type, suffixes })), - - // Voices - voices: await getVoices(), - - // Network - network: getNetwork(), - - // Intl - intl: getIntlInfo(), - - // Возможности - features: getFeatures(), - css: getCSSFeatures(), + // 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; +};