This commit is contained in:
Valentin Silyutin
2025-09-23 16:10:35 +04:00
commit 4b5fd448ef
12 changed files with 1414 additions and 0 deletions

8
.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

4
.gitattributes vendored Normal file
View File

@@ -0,0 +1,4 @@
.yarn/** linguist-vendored
.yarn/releases/* binary
.yarn/plugins/**/* binary
.pnp.* binary linguist-generated

98
.gitignore vendored Executable file
View File

@@ -0,0 +1,98 @@
# Node.js
node_modules
# Yarn (https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored)
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# ESLint
.eslintcache
# Stylelint
.stylelintcache
# Composer
vendor
# https://github.com/github/gitignore/blob/master/Global/Linux.gitignore
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk

7
.npmignore Normal file
View File

@@ -0,0 +1,7 @@
.yarn
.editorconfig
.gitattributes
.pnp.cjs
.pnp.loader.mjs
.prettierrc
.yarnrc.yml

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"printWidth": 120,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "all"
}

942
.yarn/releases/yarn-4.9.4.cjs vendored Executable file

File diff suppressed because one or more lines are too long

5
.yarnrc.yml Normal file
View File

@@ -0,0 +1,5 @@
compressionLevel: mixed
enableGlobalCache: false
yarnPath: .yarn/releases/yarn-4.9.4.cjs

1
CHANGELOG.md Normal file
View File

@@ -0,0 +1 @@
## v1.0.0

32
README.md Normal file
View File

@@ -0,0 +1,32 @@
# fingerprint
## Использование
```js
import { get } '@advdominion/fingerprint';
const fp = await get();
console.log(fp);
````
### Получение отпечатка и его отправка на сервер
```js
import { get } '@advdominion/fingerprint';
const send = async (url) => {
const fp = await get();
try {
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fp),
});
} catch (error) {
console.error(error);
}
};
await send('/');
```

287
index.js Normal file
View File

@@ -0,0 +1,287 @@
/* 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(),
};
};

12
package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "@advdominion/fingerprint",
"version": "1.0.0",
"type": "module",
"packageManager": "yarn@4.9.4",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://gitea.optiweb.ru/public/fingerprint.git"
},
"license": "MIT"
}

12
yarn.lock Normal file
View File

@@ -0,0 +1,12 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 8
cacheKey: 10
"@advdominion/fingerprint@workspace:.":
version: 0.0.0-use.local
resolution: "@advdominion/fingerprint@workspace:."
languageName: unknown
linkType: soft