v1.0.0
This commit is contained in:
8
.editorconfig
Normal file
8
.editorconfig
Normal 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
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.yarn/** linguist-vendored
|
||||
.yarn/releases/* binary
|
||||
.yarn/plugins/**/* binary
|
||||
.pnp.* binary linguist-generated
|
98
.gitignore
vendored
Executable file
98
.gitignore
vendored
Executable 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
7
.npmignore
Normal file
@@ -0,0 +1,7 @@
|
||||
.yarn
|
||||
.editorconfig
|
||||
.gitattributes
|
||||
.pnp.cjs
|
||||
.pnp.loader.mjs
|
||||
.prettierrc
|
||||
.yarnrc.yml
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
942
.yarn/releases/yarn-4.9.4.cjs
vendored
Executable file
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
5
.yarnrc.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
compressionLevel: mixed
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.4.cjs
|
1
CHANGELOG.md
Normal file
1
CHANGELOG.md
Normal file
@@ -0,0 +1 @@
|
||||
## v1.0.0
|
32
README.md
Normal file
32
README.md
Normal 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
287
index.js
Normal 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
12
package.json
Normal 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
12
yarn.lock
Normal 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
|
Reference in New Issue
Block a user