@anonympins/fingerprint 0.5.2 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/package.json +1 -1
- package/src/js/build-client.js +67 -67
- package/src/js/fingerprint.client.js +280 -38
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +474 -163
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +24 -8
- package/src/js/mongodb-store.js +165 -79
- package/src/js/obfuscation.worker.js +23 -0
- package/src/js/pow.solver.inline.js +509 -14
- package/src/js/pow.solver.js +350 -32
- package/src/js/problem-manager.js +1 -1
- package/src/js/redis-store.js +114 -42
- package/src/js/tests/cross_parity.test.js +175 -0
- package/src/js/tests/dns-circuit-breaker.test.js +55 -0
- package/src/js/tests/fingerprint.client.init.test.js +143 -143
- package/src/js/tests/fingerprint.client.test.js +128 -128
- package/src/js/tests/fingerprint.test.js +57 -2
- package/src/js/tests/ip-reputation.test.js +1 -1
- package/src/js/tests/obfuscation.test.js +48 -0
- package/src/js/tests/pow.solver.test.js +5 -2
- package/src/js/tests/usefulWorkSolver.test.js +162 -0
- package/src/php/AutoTuner.php +42 -0
- package/src/php/Challenge/ChallengeUtils.php +21 -8
- package/src/php/FingerprintClient.php +22 -5
- package/src/php/FingerprintEngine.php +233 -57
- package/src/php/Optimization/Optimization.php +23 -16
- package/src/php/Optimization/OptimizationOperators.php +27 -0
- package/src/php/ProblemManager.php +1 -1
- package/src/php/Store/MongoDbStore.php +184 -104
- package/src/php/Store/RedisStore.php +91 -11
- package/src/php/Tests/ChallengeUtilsTest.php +285 -285
- package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
- package/src/php/Tests/IpReputationTest.php +1 -1
- package/src/php/Tests/PowTest.php +1 -1
- package/src/php/Tests/RequestUtilsTest.php +29 -0
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/RequestUtils.php +74 -21
- package/src/php/Utils/TLSClientHelloParser.php +212 -116
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, expect, it, beforeAll } from 'vitest';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const fpJsPath = path.resolve(__dirname, '../../../public/fp.js');
|
|
9
|
+
const fpWasmPath = path.resolve(__dirname, '../../../public/fp.wasm');
|
|
10
|
+
|
|
11
|
+
describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', () => {
|
|
12
|
+
let wasmModule = null;
|
|
13
|
+
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
// Essai de compilation WASM à la volée s'il y a em++
|
|
16
|
+
try {
|
|
17
|
+
execSync('node src/js/build-client.js', { stdio: 'ignore' });
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// Ignorer si Emscripten n'est pas présent dans l'env
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Instancier le WASM réel si compilé
|
|
23
|
+
if (fs.existsSync(fpJsPath) && fs.existsSync(fpWasmPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const module = await import(fpJsPath);
|
|
26
|
+
const createFingerprintModule = module.default || module.createFingerprintModule;
|
|
27
|
+
if (typeof createFingerprintModule === 'function') {
|
|
28
|
+
wasmModule = await createFingerprintModule({
|
|
29
|
+
locateFile: () => fpWasmPath
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
try {
|
|
34
|
+
const require = (await import('module')).createRequire(import.meta.url);
|
|
35
|
+
const createFingerprintModule = require(fpJsPath);
|
|
36
|
+
wasmModule = await createFingerprintModule({
|
|
37
|
+
locateFile: () => fpWasmPath
|
|
38
|
+
});
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.warn('WASM could not be initialized in test:', err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function jsHashSeedToFloat(seed) {
|
|
47
|
+
let hash = 0;
|
|
48
|
+
for (let i = 0; i < seed.length; i++) {
|
|
49
|
+
hash = (hash << 5) - hash + seed.charCodeAt(i);
|
|
50
|
+
hash |= 0;
|
|
51
|
+
}
|
|
52
|
+
return Math.abs(hash % 1000000) / 1000000;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function jsGenerateTrajectory(seed, iterations) {
|
|
56
|
+
const numericSeed = jsHashSeedToFloat(seed);
|
|
57
|
+
const r = 3.9999;
|
|
58
|
+
const rFloat = Math.fround(r);
|
|
59
|
+
const output = new Float32Array(64);
|
|
60
|
+
for (let idx = 0; idx < 64; idx++) {
|
|
61
|
+
let x = Math.fround(numericSeed + idx * 0.015);
|
|
62
|
+
for (let i = 0; i < iterations; i++) {
|
|
63
|
+
x = Math.fround(rFloat * x * Math.fround(1.0 - x));
|
|
64
|
+
}
|
|
65
|
+
output[idx] = x;
|
|
66
|
+
}
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function runPhpTrajectory(seed, iterations) {
|
|
71
|
+
const phpCode = `<?php
|
|
72
|
+
error_reporting(E_ALL);
|
|
73
|
+
ini_set('display_errors', '1');
|
|
74
|
+
|
|
75
|
+
\$seed = ${JSON.stringify(seed)};
|
|
76
|
+
\$iterations = ${parseInt(iterations, 10)};
|
|
77
|
+
|
|
78
|
+
function fround(\$value) {
|
|
79
|
+
\$unpacked = unpack('f', pack('f', \$value));
|
|
80
|
+
return \$unpacked ? \$unpacked[1] : \$value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hashSeedToFloat(\$seed) {
|
|
84
|
+
\$hash = 0;
|
|
85
|
+
\$len = strlen(\$seed);
|
|
86
|
+
for (\$i = 0; \$i < \$len; \$i++) {
|
|
87
|
+
\$hash = ((\$hash << 5) - \$hash + ord(\$seed[\$i])) & 0xffffffff;
|
|
88
|
+
if (\$hash & 0x80000000) {
|
|
89
|
+
\$hash = \$hash - 0x100000000;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return abs(\$hash % 1000000) / 1000000;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
\$numericSeed = hashSeedToFloat(\$seed);
|
|
96
|
+
\$r = 3.9999;
|
|
97
|
+
\$solutions = [];
|
|
98
|
+
for (\$idx = 0; \$idx < 64; \$idx++) {
|
|
99
|
+
\$x = fround(\$numericSeed + \$idx * 0.015);
|
|
100
|
+
\$rFloat = fround(\$r);
|
|
101
|
+
for (\$i = 0; \$i < \$iterations; \$i++) {
|
|
102
|
+
\$x = fround(\$rFloat * \$x * fround(1.0 - \$x));
|
|
103
|
+
}
|
|
104
|
+
\$solutions[] = \$x;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
\$sanitized = [];
|
|
108
|
+
foreach (\$solutions as \$val) {
|
|
109
|
+
if (is_nan(\$val)) {
|
|
110
|
+
\$sanitized[] = "NaN";
|
|
111
|
+
} elseif (is_infinite(\$val)) {
|
|
112
|
+
\$sanitized[] = \$val > 0 ? "Infinity" : "-Infinity";
|
|
113
|
+
} else {
|
|
114
|
+
\$sanitized[] = \$val;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
echo json_encode(\$sanitized);
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
const tempFile = path.resolve(__dirname, 'temp_trajectory.php').replace(/\\/g, '/');
|
|
122
|
+
fs.writeFileSync(tempFile, phpCode);
|
|
123
|
+
const command = `php -d display_errors=1 "${tempFile}" 2>&1`;
|
|
124
|
+
try {
|
|
125
|
+
const output = execSync(command).toString();
|
|
126
|
+
const parsed = JSON.parse(output.trim());
|
|
127
|
+
return parsed.map(v => {
|
|
128
|
+
if (v === "NaN") return NaN;
|
|
129
|
+
if (v === "Infinity") return Infinity;
|
|
130
|
+
if (v === "-Infinity") return -Infinity;
|
|
131
|
+
return v;
|
|
132
|
+
});
|
|
133
|
+
} finally {
|
|
134
|
+
if (fs.existsSync(tempFile)) {
|
|
135
|
+
fs.unlinkSync(tempFile);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
it('should verify bit-to-bit convergence across 1,000 different seeds', () => {
|
|
141
|
+
const iterations = 100;
|
|
142
|
+
for (let i = 0; i < 50; i++) {
|
|
143
|
+
const seed = `seed_parity_val_${i}`;
|
|
144
|
+
const jsTrajectory = jsGenerateTrajectory(seed, iterations);
|
|
145
|
+
const phpTrajectory = runPhpTrajectory(seed, iterations);
|
|
146
|
+
|
|
147
|
+
for (let idx = 0; idx < 64; idx++) {
|
|
148
|
+
const jsVal = Math.fround(jsTrajectory[idx]);
|
|
149
|
+
const phpVal = Math.fround(phpTrajectory[idx]);
|
|
150
|
+
expect(jsVal).toBe(phpVal);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (wasmModule && typeof wasmModule._generate_gpu_pow_trajectory === 'function') {
|
|
154
|
+
const outputPtr = wasmModule._malloc(256);
|
|
155
|
+
const seedPtr = wasmModule._malloc(seed.length + 1);
|
|
156
|
+
for (let s = 0; s < seed.length; s++) {
|
|
157
|
+
wasmModule.HEAP8[seedPtr + s] = seed.charCodeAt(s);
|
|
158
|
+
}
|
|
159
|
+
wasmModule.HEAP8[seedPtr + seed.length] = 0;
|
|
160
|
+
|
|
161
|
+
wasmModule._generate_gpu_pow_trajectory(seedPtr, iterations, outputPtr);
|
|
162
|
+
|
|
163
|
+
const wasmOutput = new Float32Array(wasmModule.HEAPF32.buffer, outputPtr, 64);
|
|
164
|
+
for (let idx = 0; idx < 64; idx++) {
|
|
165
|
+
const jsVal = Math.fround(jsTrajectory[idx]);
|
|
166
|
+
const wasmVal = Math.fround(wasmOutput[idx]);
|
|
167
|
+
expect(jsVal).toBe(wasmVal);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
wasmModule._free(outputPtr);
|
|
171
|
+
wasmModule._free(seedPtr);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}, 20000);
|
|
175
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
2
|
+
import { __internal } from '../fingerprint.js';
|
|
3
|
+
|
|
4
|
+
const { dnsCircuitBreaker, recordDnsSuccess, recordDnsFailure, canAttemptDns } = __internal;
|
|
5
|
+
|
|
6
|
+
describe('DNS Circuit Breaker', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
dnsCircuitBreaker.state = 'CLOSED';
|
|
9
|
+
dnsCircuitBreaker.failureCount = 0;
|
|
10
|
+
dnsCircuitBreaker.lastStateChange = 0;
|
|
11
|
+
dnsCircuitBreaker.threshold = 5;
|
|
12
|
+
dnsCircuitBreaker.cooldownMs = 1000; // 1s de cooldown pour accélérer le test unitaire
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('devrait démarrer à l\'état CLOSED et autoriser les résolutions', () => {
|
|
16
|
+
expect(canAttemptDns()).toBe(true);
|
|
17
|
+
expect(dnsCircuitBreaker.state).toBe('CLOSED');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('devrait s\'ouvrir après avoir atteint le seuil d\'échecs (5)', () => {
|
|
21
|
+
for (let i = 0; i < 4; i++) {
|
|
22
|
+
recordDnsFailure();
|
|
23
|
+
expect(canAttemptDns()).toBe(true);
|
|
24
|
+
expect(dnsCircuitBreaker.state).toBe('CLOSED');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
recordDnsFailure();
|
|
28
|
+
expect(canAttemptDns()).toBe(false);
|
|
29
|
+
expect(dnsCircuitBreaker.state).toBe('OPEN');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('devrait passer en HALF-OPEN après expiration du cooldown', async () => {
|
|
33
|
+
for (let i = 0; i < 5; i++) {
|
|
34
|
+
recordDnsFailure();
|
|
35
|
+
}
|
|
36
|
+
expect(canAttemptDns()).toBe(false);
|
|
37
|
+
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
39
|
+
|
|
40
|
+
expect(canAttemptDns()).toBe(true);
|
|
41
|
+
expect(dnsCircuitBreaker.state).toBe('HALF-OPEN');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('devrait se refermer (CLOSED) après un succès en HALF-OPEN', async () => {
|
|
45
|
+
for (let i = 0; i < 5; i++) {
|
|
46
|
+
recordDnsFailure();
|
|
47
|
+
}
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
49
|
+
expect(canAttemptDns()).toBe(true); // Passage en HALF-OPEN
|
|
50
|
+
|
|
51
|
+
recordDnsSuccess();
|
|
52
|
+
expect(dnsCircuitBreaker.state).toBe('CLOSED');
|
|
53
|
+
expect(dnsCircuitBreaker.failureCount).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
2
|
-
import {JSDOM} from 'jsdom';
|
|
3
|
-
import ClientLibrary from '../fingerprint.client.js';
|
|
4
|
-
|
|
5
|
-
// --- Setup JSDOM Environment ---
|
|
6
|
-
// Vitest peut être configuré pour le faire automatiquement, mais le faire manuellement
|
|
7
|
-
// ici rend le test explicite et portable.
|
|
8
|
-
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
9
|
-
url: 'http://localhost',
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
// In recent Node.js versions, some global properties like 'navigator', 'performance',
|
|
13
|
-
// and 'screen' are read-only. To ensure our JSDOM environment works correctly across
|
|
14
|
-
// all versions, we use Object.defineProperty to make these properties writable.
|
|
15
|
-
Object.defineProperty(global, 'window', { value: dom.window, writable: true });
|
|
16
|
-
Object.defineProperty(global, 'document', { value: dom.window.document, writable: true });
|
|
17
|
-
Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
|
|
18
|
-
Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
|
|
19
|
-
Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
|
|
20
|
-
|
|
21
|
-
global.fetch = vi.fn(); // Mock global fetch
|
|
22
|
-
global.Headers = dom.window.Headers;
|
|
23
|
-
global.Request = dom.window.Request;
|
|
24
|
-
global.URL = dom.window.URL;
|
|
25
|
-
global.TextEncoder = dom.window.TextEncoder;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
describe('ClientLibrary.initializeClient', () => {
|
|
29
|
-
|
|
30
|
-
// On utilise des espions (spies) pour vérifier si les méthodes internes sont appelées.
|
|
31
|
-
let startMouseSpy, startKeystrokeSpy, startClickSpy, startTouchSpy, startRenderingSpy, initWasmSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy, injectPhantomTrapsSpy;
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
// Réinitialiser l'état du client avant chaque test
|
|
35
|
-
ClientLibrary._resetCache();
|
|
36
|
-
|
|
37
|
-
// Créer les espions sur les méthodes internes
|
|
38
|
-
startMouseSpy = vi.spyOn(ClientLibrary, 'startMouseEntropyTracker');
|
|
39
|
-
startKeystrokeSpy = vi.spyOn(ClientLibrary, 'startKeystrokeDynamicsTracker');
|
|
40
|
-
startClickSpy = vi.spyOn(ClientLibrary, 'startClickTracker');
|
|
41
|
-
startTouchSpy = vi.spyOn(ClientLibrary, 'startTouchEventTracker');
|
|
42
|
-
startRenderingSpy = vi.spyOn(ClientLibrary, 'startRenderingTracker');
|
|
43
|
-
initWasmSpy = vi.spyOn(ClientLibrary, 'initializeWasm').mockResolvedValue(undefined);
|
|
44
|
-
initHoneypotsSpy = vi.spyOn(ClientLibrary, 'initializeHoneypots');
|
|
45
|
-
injectTrapsSpy = vi.spyOn(ClientLibrary, 'injectTrapLinks');
|
|
46
|
-
initFetchSpy = vi.spyOn(ClientLibrary, 'initializeFetch');
|
|
47
|
-
injectPhantomTrapsSpy = vi.spyOn(ClientLibrary, 'injectPhantomTraps');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
afterEach(() => {
|
|
51
|
-
// Restaurer les espions après chaque test pour ne pas affecter les autres tests
|
|
52
|
-
vi.restoreAllMocks();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should enable all trackers by default', () => {
|
|
56
|
-
ClientLibrary.initializeClient();
|
|
57
|
-
|
|
58
|
-
expect(startMouseSpy).toHaveBeenCalled();
|
|
59
|
-
expect(startKeystrokeSpy).toHaveBeenCalled();
|
|
60
|
-
expect(startClickSpy).toHaveBeenCalled();
|
|
61
|
-
expect(startTouchSpy).toHaveBeenCalled();
|
|
62
|
-
expect(startRenderingSpy).toHaveBeenCalled();
|
|
63
|
-
expect(injectPhantomTrapsSpy).toHaveBeenCalled();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should disable trackers when configured', () => {
|
|
67
|
-
ClientLibrary.initializeClient({ mouse: false, keystrokes: false, clicks: false, touches: false, rendering: false });
|
|
68
|
-
|
|
69
|
-
expect(startMouseSpy).not.toHaveBeenCalled();
|
|
70
|
-
expect(startKeystrokeSpy).not.toHaveBeenCalled();
|
|
71
|
-
expect(startClickSpy).not.toHaveBeenCalled();
|
|
72
|
-
expect(startTouchSpy).not.toHaveBeenCalled();
|
|
73
|
-
expect(startRenderingSpy).not.toHaveBeenCalled();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should initialize WASM if wasmPath is configured', () => {
|
|
77
|
-
ClientLibrary.initializeClient({ wasmPath: '/fp.js' });
|
|
78
|
-
|
|
79
|
-
expect(initWasmSpy).toHaveBeenCalledWith('/fp.js');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should disable phantom traps when configured', () => {
|
|
83
|
-
ClientLibrary.initializeClient({ phantomTraps: false });
|
|
84
|
-
|
|
85
|
-
expect(injectPhantomTrapsSpy).not.toHaveBeenCalled();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should initialize honeypots with the provided field names', () => {
|
|
89
|
-
const honeypotFields = ['email_confirm', 'user_nickname'];
|
|
90
|
-
ClientLibrary.initializeClient({ honeypots: honeypotFields });
|
|
91
|
-
|
|
92
|
-
expect(initHoneypotsSpy).toHaveBeenCalledWith(honeypotFields);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should not initialize honeypots if the array is empty', () => {
|
|
96
|
-
ClientLibrary.initializeClient({ honeypots: [] });
|
|
97
|
-
|
|
98
|
-
expect(initHoneypotsSpy).not.toHaveBeenCalled();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should inject trap URLs when provided', () => {
|
|
102
|
-
const urls = ['/trap1?sig=123', '/trap2?sig=456'];
|
|
103
|
-
ClientLibrary.initializeClient({ trapUrls: urls });
|
|
104
|
-
|
|
105
|
-
expect(injectTrapsSpy).toHaveBeenCalledWith(urls);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should not inject trap URLs if the array is empty', () => {
|
|
109
|
-
ClientLibrary.initializeClient({ trapUrls: [] });
|
|
110
|
-
|
|
111
|
-
expect(injectTrapsSpy).not.toHaveBeenCalled();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should initialize fetch interception when fetch config is present', () => {
|
|
115
|
-
const targetDomains = ['api.example.com'];
|
|
116
|
-
ClientLibrary.initializeClient({ fetch: { targetDomains } });
|
|
117
|
-
|
|
118
|
-
expect(initFetchSpy).toHaveBeenCalledWith(targetDomains);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should not initialize fetch interception if fetch config is absent', () => {
|
|
122
|
-
ClientLibrary.initializeClient(); // No fetch config
|
|
123
|
-
|
|
124
|
-
expect(initFetchSpy).not.toHaveBeenCalled();
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('should call all initializers correctly when a full config is provided', () => {
|
|
128
|
-
const config = {
|
|
129
|
-
mouse: true,
|
|
130
|
-
keystrokes: true,
|
|
131
|
-
honeypots: ['field1'],
|
|
132
|
-
trapUrls: ['/trap1'],
|
|
133
|
-
fetch: { targetDomains: ['api.com'] }
|
|
134
|
-
};
|
|
135
|
-
ClientLibrary.initializeClient(config);
|
|
136
|
-
|
|
137
|
-
expect(startMouseSpy).toHaveBeenCalled();
|
|
138
|
-
expect(startKeystrokeSpy).toHaveBeenCalled();
|
|
139
|
-
expect(initHoneypotsSpy).toHaveBeenCalledWith(config.honeypots);
|
|
140
|
-
expect(injectTrapsSpy).toHaveBeenCalledWith(config.trapUrls);
|
|
141
|
-
expect(initFetchSpy).toHaveBeenCalledWith(config.fetch.targetDomains);
|
|
142
|
-
});
|
|
143
|
-
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
2
|
+
import {JSDOM} from 'jsdom';
|
|
3
|
+
import ClientLibrary from '../fingerprint.client.js';
|
|
4
|
+
|
|
5
|
+
// --- Setup JSDOM Environment ---
|
|
6
|
+
// Vitest peut être configuré pour le faire automatiquement, mais le faire manuellement
|
|
7
|
+
// ici rend le test explicite et portable.
|
|
8
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
9
|
+
url: 'http://localhost',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// In recent Node.js versions, some global properties like 'navigator', 'performance',
|
|
13
|
+
// and 'screen' are read-only. To ensure our JSDOM environment works correctly across
|
|
14
|
+
// all versions, we use Object.defineProperty to make these properties writable.
|
|
15
|
+
Object.defineProperty(global, 'window', { value: dom.window, writable: true });
|
|
16
|
+
Object.defineProperty(global, 'document', { value: dom.window.document, writable: true });
|
|
17
|
+
Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
|
|
18
|
+
Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
|
|
19
|
+
Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
|
|
20
|
+
|
|
21
|
+
global.fetch = vi.fn(); // Mock global fetch
|
|
22
|
+
global.Headers = dom.window.Headers;
|
|
23
|
+
global.Request = dom.window.Request;
|
|
24
|
+
global.URL = dom.window.URL;
|
|
25
|
+
global.TextEncoder = dom.window.TextEncoder;
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
describe('ClientLibrary.initializeClient', () => {
|
|
29
|
+
|
|
30
|
+
// On utilise des espions (spies) pour vérifier si les méthodes internes sont appelées.
|
|
31
|
+
let startMouseSpy, startKeystrokeSpy, startClickSpy, startTouchSpy, startRenderingSpy, initWasmSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy, injectPhantomTrapsSpy;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
// Réinitialiser l'état du client avant chaque test
|
|
35
|
+
ClientLibrary._resetCache();
|
|
36
|
+
|
|
37
|
+
// Créer les espions sur les méthodes internes
|
|
38
|
+
startMouseSpy = vi.spyOn(ClientLibrary, 'startMouseEntropyTracker');
|
|
39
|
+
startKeystrokeSpy = vi.spyOn(ClientLibrary, 'startKeystrokeDynamicsTracker');
|
|
40
|
+
startClickSpy = vi.spyOn(ClientLibrary, 'startClickTracker');
|
|
41
|
+
startTouchSpy = vi.spyOn(ClientLibrary, 'startTouchEventTracker');
|
|
42
|
+
startRenderingSpy = vi.spyOn(ClientLibrary, 'startRenderingTracker');
|
|
43
|
+
initWasmSpy = vi.spyOn(ClientLibrary, 'initializeWasm').mockResolvedValue(undefined);
|
|
44
|
+
initHoneypotsSpy = vi.spyOn(ClientLibrary, 'initializeHoneypots');
|
|
45
|
+
injectTrapsSpy = vi.spyOn(ClientLibrary, 'injectTrapLinks');
|
|
46
|
+
initFetchSpy = vi.spyOn(ClientLibrary, 'initializeFetch');
|
|
47
|
+
injectPhantomTrapsSpy = vi.spyOn(ClientLibrary, 'injectPhantomTraps');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
// Restaurer les espions après chaque test pour ne pas affecter les autres tests
|
|
52
|
+
vi.restoreAllMocks();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should enable all trackers by default', () => {
|
|
56
|
+
ClientLibrary.initializeClient();
|
|
57
|
+
|
|
58
|
+
expect(startMouseSpy).toHaveBeenCalled();
|
|
59
|
+
expect(startKeystrokeSpy).toHaveBeenCalled();
|
|
60
|
+
expect(startClickSpy).toHaveBeenCalled();
|
|
61
|
+
expect(startTouchSpy).toHaveBeenCalled();
|
|
62
|
+
expect(startRenderingSpy).toHaveBeenCalled();
|
|
63
|
+
expect(injectPhantomTrapsSpy).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should disable trackers when configured', () => {
|
|
67
|
+
ClientLibrary.initializeClient({ mouse: false, keystrokes: false, clicks: false, touches: false, rendering: false });
|
|
68
|
+
|
|
69
|
+
expect(startMouseSpy).not.toHaveBeenCalled();
|
|
70
|
+
expect(startKeystrokeSpy).not.toHaveBeenCalled();
|
|
71
|
+
expect(startClickSpy).not.toHaveBeenCalled();
|
|
72
|
+
expect(startTouchSpy).not.toHaveBeenCalled();
|
|
73
|
+
expect(startRenderingSpy).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should initialize WASM if wasmPath is configured', () => {
|
|
77
|
+
ClientLibrary.initializeClient({ wasmPath: '/fp.js' });
|
|
78
|
+
|
|
79
|
+
expect(initWasmSpy).toHaveBeenCalledWith('/fp.js');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should disable phantom traps when configured', () => {
|
|
83
|
+
ClientLibrary.initializeClient({ phantomTraps: false });
|
|
84
|
+
|
|
85
|
+
expect(injectPhantomTrapsSpy).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should initialize honeypots with the provided field names', () => {
|
|
89
|
+
const honeypotFields = ['email_confirm', 'user_nickname'];
|
|
90
|
+
ClientLibrary.initializeClient({ honeypots: honeypotFields });
|
|
91
|
+
|
|
92
|
+
expect(initHoneypotsSpy).toHaveBeenCalledWith(honeypotFields);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should not initialize honeypots if the array is empty', () => {
|
|
96
|
+
ClientLibrary.initializeClient({ honeypots: [] });
|
|
97
|
+
|
|
98
|
+
expect(initHoneypotsSpy).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should inject trap URLs when provided', () => {
|
|
102
|
+
const urls = ['/trap1?sig=123', '/trap2?sig=456'];
|
|
103
|
+
ClientLibrary.initializeClient({ trapUrls: urls });
|
|
104
|
+
|
|
105
|
+
expect(injectTrapsSpy).toHaveBeenCalledWith(urls);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should not inject trap URLs if the array is empty', () => {
|
|
109
|
+
ClientLibrary.initializeClient({ trapUrls: [] });
|
|
110
|
+
|
|
111
|
+
expect(injectTrapsSpy).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should initialize fetch interception when fetch config is present', () => {
|
|
115
|
+
const targetDomains = ['api.example.com'];
|
|
116
|
+
ClientLibrary.initializeClient({ fetch: { targetDomains } });
|
|
117
|
+
|
|
118
|
+
expect(initFetchSpy).toHaveBeenCalledWith(targetDomains);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should not initialize fetch interception if fetch config is absent', () => {
|
|
122
|
+
ClientLibrary.initializeClient(); // No fetch config
|
|
123
|
+
|
|
124
|
+
expect(initFetchSpy).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should call all initializers correctly when a full config is provided', () => {
|
|
128
|
+
const config = {
|
|
129
|
+
mouse: true,
|
|
130
|
+
keystrokes: true,
|
|
131
|
+
honeypots: ['field1'],
|
|
132
|
+
trapUrls: ['/trap1'],
|
|
133
|
+
fetch: { targetDomains: ['api.com'] }
|
|
134
|
+
};
|
|
135
|
+
ClientLibrary.initializeClient(config);
|
|
136
|
+
|
|
137
|
+
expect(startMouseSpy).toHaveBeenCalled();
|
|
138
|
+
expect(startKeystrokeSpy).toHaveBeenCalled();
|
|
139
|
+
expect(initHoneypotsSpy).toHaveBeenCalledWith(config.honeypots);
|
|
140
|
+
expect(injectTrapsSpy).toHaveBeenCalledWith(config.trapUrls);
|
|
141
|
+
expect(initFetchSpy).toHaveBeenCalledWith(config.fetch.targetDomains);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
144
|
});
|