288 lines
9.3 KiB
JavaScript
288 lines
9.3 KiB
JavaScript
/* eslint-disable unicorn/no-null */
|
|
|
|
const sha256 = async (str) => {
|
|
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
|
|
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
};
|
|
|
|
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, 50, 30);
|
|
ctx.fillStyle = '#069';
|
|
ctx.fillText('fingerprint123', 2, 2);
|
|
return await sha256(canvas.toDataURL());
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getWebGLFingerprint = async () => {
|
|
try {
|
|
const canvas = document.createElement('canvas');
|
|
const gl = canvas.getContext('webgl');
|
|
if (!gl) return null;
|
|
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),
|
|
extensions: gl.getSupportedExtensions(),
|
|
};
|
|
return { hash: await sha256(JSON.stringify(props)), props };
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getAudioFingerprint = 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();
|
|
osc.type = 'triangle';
|
|
osc.frequency.value = 10_000;
|
|
osc.connect(comp);
|
|
comp.connect(ctx.destination);
|
|
osc.start(0);
|
|
const buf = await ctx.startRendering();
|
|
let sum = 0;
|
|
const data = buf.getChannelData(0);
|
|
for (let i = 0; i < data.length; i += 100) sum += Math.abs(data[i]);
|
|
return await sha256(String(sum));
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
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 await sha256(JSON.stringify(values));
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getBattery = async () => {
|
|
try {
|
|
if (!navigator.getBattery) return null;
|
|
const b = await navigator.getBattery();
|
|
return {
|
|
charging: b.charging,
|
|
level: b.level,
|
|
chargingTime: b.chargingTime,
|
|
dischargingTime: b.dischargingTime,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getWebGPU = async () => {
|
|
try {
|
|
if (!navigator.gpu?.requestAdapter) return null;
|
|
const adapter = await navigator.gpu.requestAdapter();
|
|
if (!adapter) return null;
|
|
return {
|
|
name: adapter.name,
|
|
features: [...(adapter.features?.values?.() ?? [])],
|
|
limits: adapter.limits ? Object.fromEntries(Object.entries(adapter.limits)) : {},
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getStorage = async () => {
|
|
try {
|
|
const est = await navigator.storage.estimate();
|
|
return { quota: est.quota, usage: est.usage };
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getVoices = () => {
|
|
try {
|
|
if (!window.speechSynthesis) return null;
|
|
return window.speechSynthesis.getVoices().map((v) => ({
|
|
name: v.name,
|
|
lang: v.lang,
|
|
local: v.localService,
|
|
default: v.default,
|
|
}));
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
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,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
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,
|
|
});
|
|
|
|
const mq = (q) => window.matchMedia(q).matches;
|
|
|
|
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)'),
|
|
});
|
|
|
|
export const get = async () => {
|
|
return {
|
|
date: Date.now(),
|
|
|
|
// Базовые данные
|
|
ua: navigator.userAgent,
|
|
platform: navigator.platform ?? null,
|
|
lang: navigator.language,
|
|
languages: navigator.languages,
|
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
dnt: navigator.doNotTrack === '1',
|
|
|
|
// 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: getVoices(),
|
|
|
|
// Network
|
|
network: getNetwork(),
|
|
|
|
// Intl
|
|
intl: getIntlInfo(),
|
|
|
|
// Возможности
|
|
features: getFeatures(),
|
|
css: getCSSFeatures(),
|
|
};
|
|
};
|