@anonympins/fingerprint 0.3.8 → 0.4.0

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +331 -256
  2. package/README.md +62 -53
  3. package/composer.json +38 -38
  4. package/index.js +4 -4
  5. package/package.json +103 -103
  6. package/phpunit.xml +20 -20
  7. package/public/fp.js +1 -1
  8. package/public/fp.wasm +0 -0
  9. package/src/js/build-client.js +1 -1
  10. package/src/js/fingerprint.client.js +2 -0
  11. package/src/js/fingerprint.js +4429 -4381
  12. package/src/js/mongodb-store.js +79 -79
  13. package/src/js/pow.solver.inline.js +31 -0
  14. package/src/js/pow.solver.js +31 -0
  15. package/src/js/tests/fingerprint.builder.test.js +79 -0
  16. package/src/js/tests/fingerprint.client.init.test.js +120 -0
  17. package/src/js/tests/fingerprint.client.test.js +105 -0
  18. package/src/js/tests/fingerprint.engine.test.js +371 -0
  19. package/src/js/tests/fingerprint.isMalicious.test.js +117 -0
  20. package/src/js/tests/fingerprint.test.js +2319 -0
  21. package/src/js/tests/ip-reputation.test.js +132 -0
  22. package/src/js/tests/ja3AnomalyDetector.test.js +135 -0
  23. package/src/js/tests/library.test.js +96 -0
  24. package/src/js/tests/metrics.test.js +104 -0
  25. package/src/js/tests/pow.solver.test.js +198 -0
  26. package/src/js/tests/problem-manager.test.js +323 -0
  27. package/src/js/tests/stores.test.js +118 -0
  28. package/src/php/Challenge/ChallengeUtils.php +361 -361
  29. package/src/php/Config/SecurityProfiles.php +271 -266
  30. package/src/php/FingerprintBuilder.php +185 -185
  31. package/src/php/FingerprintClient.php +131 -131
  32. package/src/php/FingerprintEngine.php +1006 -1006
  33. package/src/php/Ja3AnomalyDetector.php +227 -227
  34. package/src/php/Optimization/FunctionRegistry.php +62 -62
  35. package/src/php/Optimization/Optimization.php +255 -255
  36. package/src/php/Optimization/OptimizationOperators.php +304 -304
  37. package/src/php/Store/InMemoryStore.php +66 -66
  38. package/src/php/Store/MongoDbStore.php +104 -104
  39. package/src/php/Store/RedisStore.php +53 -53
  40. package/src/php/Tests/ChallengeUtilsTest.php +81 -81
  41. package/src/php/Tests/FingerprintBuilderTest.php +57 -57
  42. package/src/php/Tests/FingerprintClientTest.php +71 -0
  43. package/src/php/Tests/FingerprintEngineTest.php +299 -299
  44. package/src/php/Tests/IpReputationTest.php +156 -156
  45. package/src/php/Tests/Ja3AnomalyDetectorTest.php +179 -179
  46. package/src/php/Tests/MetricsTest.php +45 -45
  47. package/src/php/Tests/PowTest.php +39 -39
  48. package/src/php/Tests/ProblemManagerTest.php +296 -296
  49. package/src/php/Tests/RequestUtilsTest.php +253 -145
  50. package/src/php/Tests/TLSClientHelloParserTest.php +118 -0
  51. package/src/php/Tests/problems.config.json +8 -8
  52. package/src/php/Utils/BigInt.php +144 -144
  53. package/src/php/Utils/Logger.php +29 -29
  54. package/src/php/Utils/MaliciousPatterns.php +58 -58
  55. package/src/php/Utils/MetricsManager.php +166 -166
  56. package/src/php/Utils/RequestUtils.php +1169 -1169
  57. package/src/php/Utils/TLSClientHelloParser.php +117 -0
  58. package/src/php/bin/auto-tune.php +117 -117
@@ -0,0 +1,132 @@
1
+ import {beforeEach, describe, expect, it} from 'vitest';
2
+ import {__internal, configureStore, FingerprintEngine} from '../fingerprint.js';
3
+
4
+ describe('IP Reputation Local System (Node.js)', () => {
5
+ let mockStore;
6
+
7
+ beforeEach(() => {
8
+ mockStore = {
9
+ _map: new Map(),
10
+ async get(key) { return this._map.get(key); },
11
+ async set(key, value) { this._map.set(key, value); },
12
+ async has(key) { return this._map.has(key); },
13
+ async delete(key) { this._map.delete(key); }
14
+ };
15
+ configureStore(mockStore);
16
+ });
17
+
18
+ it('should return a default score of 0 for unknown IPs', async () => {
19
+ const score = await __internal.getIpReputationScore('1.1.1.1');
20
+ expect(score).toBe(0);
21
+ });
22
+
23
+ it('should correctly increment and decrement score values', async () => {
24
+ const ip = '192.168.1.1';
25
+ await __internal.updateIpReputationScore(ip, 30);
26
+ let score = await __internal.getIpReputationScore(ip);
27
+ expect(score).toBe(30);
28
+
29
+ await __internal.updateIpReputationScore(ip, -10);
30
+ score = await __internal.getIpReputationScore(ip);
31
+ expect(score).toBe(20);
32
+ });
33
+
34
+ it('should clamp the reputation score within bounds [0, 100]', async () => {
35
+ const ip = '10.0.0.1';
36
+ await __internal.updateIpReputationScore(ip, 150);
37
+ let score = await __internal.getIpReputationScore(ip);
38
+ expect(score).toBe(100);
39
+
40
+ await __internal.updateIpReputationScore(ip, -200);
41
+ score = await __internal.getIpReputationScore(ip);
42
+ expect(score).toBe(0);
43
+ });
44
+
45
+ it('should apply passive decay of 2 points per hour of inactivity', async () => {
46
+ const ip = '172.16.0.1';
47
+ const now = Date.now();
48
+
49
+ // Simulation d'un score de 50 mis à jour il y a 3 heures (3h * 2 points/heure = 6 points de perte)
50
+ await mockStore.set(`ip-reputation:${ip}`, {
51
+ score: 50,
52
+ lastUpdate: now - (3 * 60 * 60 * 1000)
53
+ });
54
+
55
+ const score = await __internal.getIpReputationScore(ip);
56
+ expect(score).toBe(44);
57
+ });
58
+
59
+ it('should integrate ipReputationScore into the final score calculation', async () => {
60
+ const ip = '1.2.3.4';
61
+ await __internal.updateIpReputationScore(ip, 60); // Set IP reputation to 60
62
+
63
+ const mockSecurityConfig = {
64
+ weights: {
65
+ ipReputationScore: 0.5, // Give it a weight
66
+ historyScore: 0.0, // Other weights to 0 for isolation
67
+ rotationScore: 0.0,
68
+ headerAnomalyScore: 0.0,
69
+ requestPatternScore: 0.0,
70
+ inconsistencyScore: 0.0,
71
+ honeypotScore: 0.0,
72
+ behaviorScore: 0.0,
73
+ botScore: 0.0,
74
+ crossLayerInconsistencyScore: 0.0,
75
+ tlsSpoofingScore: 0.0,
76
+ timeInconsistencyScore: 0.0,
77
+ clickVarianceScore: 0.0,
78
+ clientHintsInconsistencyScore: 0.0,
79
+ subnetScore: 0.0,
80
+ },
81
+ thresholds: { low: 0, medium: 0, high: 0, block: 100 }, // Irrelevant for this test
82
+ };
83
+
84
+ const ipRepScore = await __internal.getIpReputationScore(ip);
85
+ const suspicionVector = { ipReputationScore: ipRepScore };
86
+
87
+ const engine = new FingerprintEngine(mockSecurityConfig);
88
+ const finalScore = engine.calculateFinalScore(suspicionVector);
89
+
90
+ // Expected score: 60 (ipRepScore) * 0.5 (weight) = 30
91
+ expect(finalScore).toBe(30.0);
92
+ });
93
+
94
+ it('should return correct subnet for IPv4 and IPv6', async () => {
95
+ const ipv4Subnet = __internal.getIpSubnet('192.168.1.50', 24, 48);
96
+ expect(ipv4Subnet).toBe('192.168.1.0/24');
97
+
98
+ const ipv6Subnet = __internal.getIpSubnet('2001:db8:abcd:12::1', 24, 48);
99
+ expect(ipv6Subnet).toBe('2001:db8:abcd:0:0:0:0:0/48');
100
+ });
101
+
102
+ it('should calculate client hints inconsistency score correctly', async () => {
103
+ const mockContextMismatch = {
104
+ headers: {
105
+ 'user-agent': 'Mozilla/5.0 Firefox/117.0',
106
+ 'sec-ch-ua': '"Google Chrome";v="117"'
107
+ }
108
+ };
109
+ const scoreMismatch = __internal.getClientHintsInconsistencyScore(mockContextMismatch);
110
+ expect(scoreMismatch.clientHintsInconsistencyScore).toBe(90);
111
+
112
+ const mockContextVersionDrift = {
113
+ headers: {
114
+ 'user-agent': 'Mozilla/5.0 Chrome/110.0',
115
+ 'sec-ch-ua': '"Google Chrome";v="117"'
116
+ }
117
+ };
118
+ const scoreVersionDrift = __internal.getClientHintsInconsistencyScore(mockContextVersionDrift);
119
+ expect(scoreVersionDrift.clientHintsInconsistencyScore).toBe(80);
120
+ });
121
+
122
+ it('should integrate subnet history into subnet score', async () => {
123
+ const mockContext = { clientIp: '192.168.1.50' };
124
+ let score = await __internal.getSubnetScore(mockContext);
125
+ expect(score.subnetScore).toBe(0);
126
+ for (let i = 1; i <= 12; i++) {
127
+ await __internal.updateSubnetMetrics(mockContext, `dev-${i}`, 40);
128
+ }
129
+ score = await __internal.getSubnetScore(mockContext);
130
+ expect(score.subnetScore).toBeGreaterThan(0);
131
+ });
132
+ });
@@ -0,0 +1,135 @@
1
+ import crypto from 'crypto';
2
+ import {getTlsSpoofingScore, parseJa3} from '../fingerprint.js';
3
+ import {vi} from 'vitest';
4
+
5
+ describe('JA3 Anomaly Detector (Node.js)', () => {
6
+
7
+ // Mock d'un store de cache asynchrone en mémoire
8
+ const createMockStore = () => {
9
+ const storage = {};
10
+ return {
11
+ get: vi.fn(async (key) => storage[key] || null),
12
+ set: vi.fn(async (key, val) => { storage[key] = val; })
13
+ };
14
+ };
15
+
16
+ describe('parseJa3', () => {
17
+ it('devrait parser correctement une chaine JA3 brute', () => {
18
+ const rawJa3 = '771,4865-4866-4867,0-23-65281-10-11,29-23-24,0';
19
+ const parsed = parseJa3(rawJa3);
20
+
21
+ expect(parsed).not.toBeNull();
22
+ expect(parsed.tlsVersion).toBe(771);
23
+ expect(parsed.ciphers).toEqual([4865, 4866, 4867]);
24
+ expect(parsed.extensions).toEqual([0, 23, 65281, 10, 11]);
25
+ });
26
+
27
+ it('devrait retourner null pour des chaines invalides', () => {
28
+ expect(parseJa3('')).toBeNull();
29
+ expect(parseJa3('771,4865')).toBeNull();
30
+ });
31
+ });
32
+
33
+ describe('getTlsSpoofingScore (Anomalies JA3 hachées)', () => {
34
+ it('devrait détecter l\'usurpation d\'identité d\'une bibliothèque (Python)', async () => {
35
+ const pythonJa3Hash = '47344a349b75c4e82333475553b5f358';
36
+ const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
37
+
38
+ const context = {
39
+ headers: {
40
+ 'user-agent': chromeUa,
41
+ 'x-ja3-hash': pythonJa3Hash
42
+ }
43
+ };
44
+
45
+ const result = await getTlsSpoofingScore(context);
46
+ expect(result.tlsSpoofingScore).toBe(90);
47
+ });
48
+
49
+ it('devrait détecter la rotation d\'User-Agent sur un même hash JA3 (Stagnation)', async () => {
50
+ const mockStore = createMockStore();
51
+ const unknownJa3 = '00000000000000000000000000000000';
52
+
53
+ const contextChrome = {
54
+ headers: {
55
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0',
56
+ 'x-ja3-hash': unknownJa3
57
+ }
58
+ };
59
+
60
+ const contextFirefox = {
61
+ headers: {
62
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/120.0',
63
+ 'x-ja3-hash': unknownJa3
64
+ }
65
+ };
66
+
67
+ // Injecter le mockStore dans le module de test si nécessaire ou mocker l'importation du store global
68
+ // Premier passage : Chrome
69
+ const res1 = await getTlsSpoofingScore(contextChrome, mockStore);
70
+
71
+ // Deuxième passage : Firefox (Détection de la rotation de UA)
72
+ const res2 = await getTlsSpoofingScore(contextFirefox, mockStore);
73
+ expect(res2.tlsSpoofingScore).toBe(85);
74
+ });
75
+ });
76
+
77
+ describe('getTlsSpoofingScore (Anomalies JA3 brutes)', () => {
78
+ it('devrait suspecter un faux Chrome n\'utilisant pas le mécanisme GREASE', async () => {
79
+ const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
80
+ // Pas de valeurs GREASE dans la suite
81
+ const ja3RawNoGrease = '771,4865-4866,0-23-10,29,0';
82
+ const ja3Hash = crypto.createHash('md5').update(ja3RawNoGrease).digest('hex');
83
+
84
+ const context = {
85
+ headers: {
86
+ 'user-agent': chromeUa,
87
+ 'x-ja3-raw': ja3RawNoGrease,
88
+ 'x-ja3-hash': ja3Hash
89
+ },
90
+ httpVersion: '2.0'
91
+ };
92
+
93
+ const result = await getTlsSpoofingScore(context);
94
+ expect(result.tlsSpoofingScore).toBe(75);
95
+ });
96
+
97
+ it('devrait valider un Chrome légitime utilisant des valeurs GREASE', async () => {
98
+ const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
99
+ // 2570 est une suite GREASE valide
100
+ const ja3RawWithGrease = '771,4865-2570,0-23-10-16,29,0';
101
+ const ja3Hash = crypto.createHash('md5').update(ja3RawWithGrease).digest('hex');
102
+
103
+ const context = {
104
+ headers: {
105
+ 'user-agent': chromeUa,
106
+ 'x-ja3-raw': ja3RawWithGrease,
107
+ 'x-ja3-hash': ja3Hash
108
+ },
109
+ httpVersion: '2.0'
110
+ };
111
+
112
+ const result = await getTlsSpoofingScore(context);
113
+ expect(result.tlsSpoofingScore).toBeLessThan(70);
114
+ });
115
+
116
+ it('devrait détecter l\'absence d\'extension ALPN pour une connexion HTTP/2', async () => {
117
+ const chromeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0';
118
+ // Pas d'extension 16 (ALPN)
119
+ const ja3RawNoAlpn = '771,4865-2570,0-23-10,29,0';
120
+ const ja3Hash = crypto.createHash('md5').update(ja3RawNoAlpn).digest('hex');
121
+
122
+ const context = {
123
+ headers: {
124
+ 'user-agent': chromeUa,
125
+ 'x-ja3-raw': ja3RawNoAlpn,
126
+ 'x-ja3-hash': ja3Hash
127
+ },
128
+ httpVersion: '2.0'
129
+ };
130
+
131
+ const result = await getTlsSpoofingScore(context);
132
+ expect(result.tlsSpoofingScore).toBe(70);
133
+ });
134
+ });
135
+ });
@@ -0,0 +1,96 @@
1
+ import {describe, expect, it} from 'vitest';
2
+ import {Optimization} from '../library.js';
3
+
4
+ describe('Optimization.Operators.benfordTest', () => {
5
+
6
+ it('should return 0 for non-array inputs', () => {
7
+ expect(Optimization.Operators.benfordTest("12345")).toBe(0);
8
+ expect(Optimization.Operators.benfordTest(null)).toBe(0);
9
+ expect(Optimization.Operators.benfordTest(undefined)).toBe(0);
10
+ expect(Optimization.Operators.benfordTest({ a: 1 })).toBe(0);
11
+ });
12
+
13
+ it('should return 0 for arrays with less than 10 valid numbers', () => {
14
+ const smallArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
15
+ expect(Optimization.Operators.benfordTest(smallArray)).toBe(0);
16
+ });
17
+
18
+ it('should return 0 for an empty array', () => {
19
+ expect(Optimization.Operators.benfordTest([])).toBe(0);
20
+ });
21
+
22
+ it('should ignore zeros, non-numeric strings, and leading spaces', () => {
23
+ // This array contains only 9 valid leading digits.
24
+ const dirtyArray = [0, " 123", "abc", 2, 3, 4, 5, 6, 7, 8, 9, null, undefined];
25
+ expect(Optimization.Operators.benfordTest(dirtyArray)).toBe(0);
26
+ });
27
+
28
+ it('should return a very low score for a distribution that perfectly matches Benford\'s law', () => {
29
+ // Création d'un échantillon de 1000 nombres qui suit la loi de Benford
30
+ const benfordSample = [
31
+ ...Array(301).fill(100), // 301 nombres commençant par 1
32
+ ...Array(176).fill(200), // 176 nombres commençant par 2
33
+ ...Array(125).fill(300), // 125 nombres commençant par 3
34
+ ...Array(97).fill(400), // etc.
35
+ ...Array(79).fill(500),
36
+ ...Array(67).fill(600),
37
+ ...Array(58).fill(700),
38
+ ...Array(51).fill(800),
39
+ ...Array(46).fill(900),
40
+ ];
41
+
42
+ const score = Optimization.Operators.benfordTest(benfordSample);
43
+ // Le score devrait être très proche de 0. On utilise toBeLessThan pour tolérer les imprécisions de calcul.
44
+ expect(score).toBeLessThan(1e-9);
45
+ });
46
+
47
+ it('should return a high (suspect) score for a uniform distribution', () => {
48
+ // Une distribution uniforme est très peu naturelle pour ce genre de données.
49
+ const uniformSample = [];
50
+ for (let i = 1; i <= 9; i++) {
51
+ for (let j = 0; j < 100; j++) {
52
+ uniformSample.push(i * 100 + j);
53
+ }
54
+ }
55
+
56
+ const score = Optimization.Operators.benfordTest(uniformSample);
57
+ // Un score > 0.15 est considéré comme suspect.
58
+ expect(score).toBeGreaterThan(0.15);
59
+ });
60
+
61
+ it('should return a high (suspect) score for a distribution skewed towards high digits', () => {
62
+ // L'inverse de la loi de Benford, très suspect.
63
+ const inverseBenfordSample = [
64
+ ...Array(46).fill(100),
65
+ ...Array(51).fill(200),
66
+ ...Array(58).fill(300),
67
+ ...Array(67).fill(400),
68
+ ...Array(79).fill(500),
69
+ ...Array(97).fill(600),
70
+ ...Array(125).fill(700),
71
+ ...Array(176).fill(800),
72
+ ...Array(301).fill(900),
73
+ ];
74
+
75
+ const score = Optimization.Operators.benfordTest(inverseBenfordSample);
76
+ expect(score).toBeGreaterThan(0.3); // Score attendu encore plus élevé
77
+ });
78
+
79
+ it('should handle real-world-like data (request timings)', () => {
80
+ // Simule des délais de requêtes générés par un bot (ex: aléatoire uniforme entre 500 et 1500ms)
81
+ const botTimings = Array.from({ length: 100 }, () => 500 + Math.random() * 1000);
82
+ const botScore = Optimization.Operators.benfordTest(botTimings);
83
+
84
+ // Simule des délais humains (plus de petits délais, quelques longs délais)
85
+ const humanTimings = [
86
+ 123, 234, 180, 345, 150, 456, 110, 190, 210, 280, 567, 130, 890, 1200, 310, 160
87
+ ];
88
+ const humanScore = Optimization.Operators.benfordTest(humanTimings);
89
+
90
+ // Le score du bot devrait être significativement plus élevé que celui de l'humain.
91
+ // Les valeurs exactes peuvent varier, mais la tendance doit être claire.
92
+ expect(botScore).toBeGreaterThan(0.1);
93
+ expect(humanScore).toBeLessThan(botScore);
94
+ });
95
+
96
+ });
@@ -0,0 +1,104 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { handleMetricsRequest, __internal } from '../fingerprint.js';
3
+
4
+ describe('Metrics & Authorization Callback Integration', () => {
5
+ let req, res;
6
+
7
+ beforeEach(() => {
8
+ req = {
9
+ ip: '127.0.0.1',
10
+ path: '/metrics',
11
+ headers: { 'user-agent': 'prometheus' },
12
+ query: {},
13
+ body: {},
14
+ cookies: {},
15
+ httpVersion: '1.1'
16
+ };
17
+ res = {
18
+ status: vi.fn().mockReturnThis(),
19
+ send: vi.fn().mockReturnThis(),
20
+ set: vi.fn().mockReturnThis(),
21
+ redirect: vi.fn().mockReturnThis()
22
+ };
23
+ });
24
+ it('should return metrics with 200 Content-Type if authorized', async () => {
25
+ const config = {
26
+ metricsAuthorizationCallback: () => true
27
+ };
28
+ await handleMetricsRequest(req, res, config);
29
+
30
+ expect(res.set).toHaveBeenCalledWith('Content-Type', expect.stringContaining('text/plain'));
31
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_requests_total'));
32
+ });
33
+
34
+ it('should include current weights and thresholds in prometheus metrics output', async () => {
35
+ const config = {
36
+ metricsAuthorizationCallback: () => true,
37
+ weights: { historyScore: 0.35, rotationScore: 0.65 },
38
+ thresholds: { low: 22, block: 92 }
39
+ };
40
+ await handleMetricsRequest(req, res, config);
41
+
42
+ expect(res.set).toHaveBeenCalledWith('Content-Type', expect.stringContaining('text/plain'));
43
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_weight{indicator="historyScore"} 0.35'));
44
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_weight{indicator="rotationScore"} 0.65'));
45
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_threshold{level="low"} 22'));
46
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_security_threshold{level="block"} 92'));
47
+ });
48
+
49
+ it('should include auto-tuning performance metrics if a best solution is available', async () => {
50
+ __internal.setLastBestSolution({
51
+ solution: {},
52
+ objectives: [0.0123, 0.0456]
53
+ });
54
+
55
+ const config = {
56
+ metricsAuthorizationCallback: () => true
57
+ };
58
+ await handleMetricsRequest(req, res, config);
59
+
60
+ expect(res.set).toHaveBeenCalledWith('Content-Type', expect.stringContaining('text/plain'));
61
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_autotuning_false_positive_rate 0.0123'));
62
+ expect(res.send).toHaveBeenCalledWith(expect.stringContaining('fingerprint_autotuning_false_negative_rate 0.0456'));
63
+
64
+ // Cleanup
65
+ __internal.setLastBestSolution(null);
66
+ });
67
+
68
+ it('should return 403 if authorization callback returns false', async () => {
69
+ const config = {
70
+ metricsAuthorizationCallback: () => false
71
+ };
72
+ await handleMetricsRequest(req, res, config);
73
+
74
+ expect(res.status).toHaveBeenCalledWith(403);
75
+ expect(res.send).toHaveBeenCalledWith('Access to metrics denied.');
76
+ });
77
+
78
+ it('should handle custom block action returned by authorization callback', async () => {
79
+ const config = {
80
+ metricsAuthorizationCallback: () => ({
81
+ action: 'block',
82
+ status: 401,
83
+ body: 'Custom unauthorized message'
84
+ })
85
+ };
86
+ await handleMetricsRequest(req, res, config);
87
+
88
+ expect(res.status).toHaveBeenCalledWith(401);
89
+ expect(res.send).toHaveBeenCalledWith('Custom unauthorized message');
90
+ });
91
+
92
+ it('should handle custom redirect action returned by authorization callback', async () => {
93
+ const config = {
94
+ metricsAuthorizationCallback: () => ({
95
+ action: 'redirect',
96
+ status: 302,
97
+ path: '/forbidden'
98
+ })
99
+ };
100
+ await handleMetricsRequest(req, res, config);
101
+
102
+ expect(res.redirect).toHaveBeenCalledWith(302, '/forbidden');
103
+ });
104
+ });
@@ -0,0 +1,198 @@
1
+ import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest';
2
+ import {createHash} from 'node:crypto';
3
+ // Import the functions to be tested.
4
+ // Since the inline file doesn't use exports, we can't directly import.
5
+ // Instead, we'll test the ES module version which shares the same logic.
6
+ import {solveChallenge, solveCpuTargetInline, solveMemory, solveTsp} from '../pow.solver.js';
7
+
8
+ describe('Proof-of-Work Solvers', () => {
9
+
10
+ describe('solveCpuTargetInline', () => {
11
+ const nonce = 'test-nonce';
12
+ // A relatively easy target for quick tests (first 16 bits must be zero)
13
+ const targetHex = '0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF';
14
+ const targetBigInt = BigInt('0x' + targetHex);
15
+
16
+ it('should find a valid CPU solution with a simple base block', async () => {
17
+ // Le `baseBlock` est maintenant un Uint8Array.
18
+ // Le client et le serveur doivent s'accorder sur sa construction.
19
+ // Ici, on simule un challenge simple avec juste le nonce.
20
+ const baseBlock = new TextEncoder().encode(`${nonce}:`);
21
+ const solution = await solveCpuTargetInline(baseBlock, targetHex);
22
+
23
+ // Verification: re-hash the solution and check against the target
24
+ const finalBlock = new Uint8Array([...baseBlock, ...new TextEncoder().encode(String(solution))]);
25
+ const hash = createHash('sha256').update(finalBlock).digest('hex');
26
+ const hashBigInt = BigInt('0x' + hash);
27
+
28
+ expect(hashBigInt).toBeLessThan(targetBigInt);
29
+ }, 20000);
30
+
31
+ it('should find a valid CPU solution with a client secret', async () => {
32
+ const clientSecret = 'my-secret';
33
+ const fingerprint = 'test-fp-string';
34
+ // Le `baseBlock` est maintenant un Uint8Array qui inclut toutes les informations.
35
+ const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${fingerprint}:`);
36
+ const solution = await solveCpuTargetInline(baseBlock, targetHex);
37
+
38
+ // Verification: re-hash the solution and check against the target
39
+ const finalBlock = new Uint8Array([...baseBlock, ...new TextEncoder().encode(String(solution))]);
40
+ const hash = createHash('sha256').update(finalBlock).digest('hex');
41
+ const hashBigInt = BigInt('0x' + hash);
42
+ expect(hashBigInt).toBeLessThan(targetBigInt);
43
+ }, 20000);
44
+
45
+ it('should call the progress callback', async () => {
46
+ const progressCallback = vi.fn();
47
+ const baseBlock = new TextEncoder().encode(`${nonce}:`);
48
+ // Use a much harder target to ensure the loop runs long enough
49
+ const hardTarget = '0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF';
50
+ await solveCpuTargetInline(baseBlock, hardTarget, progressCallback);
51
+
52
+ // Check if the callback was called with a number
53
+ expect(progressCallback).toHaveBeenCalled();
54
+ expect(progressCallback).toHaveBeenCalledWith(expect.any(Number));
55
+ }, 40000); // Increase timeout for harder challenge
56
+ });
57
+
58
+ describe('solveMemory', () => {
59
+ it('should produce a deterministic result for a given seed and difficulty', async () => {
60
+ const seed = 'test-seed';
61
+ const difficulty = 1; // 1MB
62
+
63
+ const solution1 = await solveMemory(seed, difficulty);
64
+ const solution2 = await solveMemory(seed, difficulty);
65
+
66
+ expect(solution1).toBe(solution2);
67
+ expect(solution1).not.toBe(await solveMemory('different-seed', difficulty));
68
+ });
69
+ });
70
+
71
+ describe('solveTsp', () => {
72
+ it('should solve a simple TSP problem', async () => {
73
+ const cities = [{ x: 10, y: 10 }, { x: 90, y: 90 }, { x: 10, y: 90 }, { x: 90, y: 10 }];
74
+ const result = await solveTsp(cities, 400);
75
+
76
+ expect(result.path).toBeInstanceOf(Array);
77
+ expect(result.path.length).toBe(4);
78
+ // The optimal path for a square is ~324. The nearest neighbor should find this.
79
+ expect(result.distance).toBeLessThan(330);
80
+ });
81
+ });
82
+
83
+ describe('solveChallenge', () => {
84
+ const nonce = 'challenge-nonce';
85
+ const clientSecret = 'challenge-secret';
86
+ const cpuTarget = '0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF';
87
+
88
+ it('should solve a "cpu_target" challenge', async () => {
89
+ // The 'cpu_target' case in solveChallenge uses Web Workers, which are not available in Node.js test env.
90
+ // We will test the 'cpu_mem' case which uses the same underlying `solveCpuTargetInline` function.
91
+ // This test is effectively covered by the 'cpu_mem' test below.
92
+ expect(true).toBe(true);
93
+ });
94
+
95
+ it('should solve a "cpu_mem" challenge for an API client', async () => {
96
+ const challenge = {
97
+ type: 'cpu_mem',
98
+ nonce,
99
+ clientSecret,
100
+ cpuTarget,
101
+ memDifficulty: 1,
102
+ // Pour les appels API, le serveur pré-calcule le baseBlock
103
+ // sans l'IP du client, mais avec l'empreinte digitale.
104
+ baseBlock: [...new TextEncoder().encode(`${nonce}:${clientSecret}:`)]
105
+ };
106
+
107
+ const solutions = await solveChallenge(challenge, ''); // Le fingerprint est passé en 2e arg
108
+ expect(solutions.rawSolution).toHaveProperty('cpu', expect.any(Number));
109
+ expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Number));
110
+ }, 20000);
111
+
112
+ it('should solve a "cpu_mem_inline" challenge for a browser', async () => {
113
+ const challenge = {
114
+ type: 'cpu_mem_inline',
115
+ nonce,
116
+ clientSecret,
117
+ cpuTarget,
118
+ memDifficulty: 1,
119
+ // Pour le challenge inline, le serveur inclut l'IP dans le baseBlock.
120
+ baseBlock: [...new TextEncoder().encode(`${nonce}:${clientSecret}:`)]
121
+ };
122
+
123
+ const solutions = await solveChallenge(challenge, '');
124
+ expect(solutions.rawSolution).toHaveProperty('cpu', expect.any(Number));
125
+ expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Number));
126
+ }, 20000);
127
+
128
+ it('should solve a "tsp" challenge', async () => {
129
+ const challenge = {
130
+ type: 'tsp',
131
+ cities: [{ x: 0, y: 0 }, { x: 100, y: 100 }],
132
+ targetMaxDistance: 300
133
+ };
134
+ const solutions = await solveChallenge(challenge);
135
+ // Pour le TSP, la solution brute est directement le chemin
136
+ expect(solutions.rawSolution).toEqual([0, 1]);
137
+ });
138
+
139
+ it('should throw an error for an unknown challenge type', async () => {
140
+ const challenge = { type: 'unknown' };
141
+ await expect(solveChallenge(challenge)).rejects.toThrow('Unknown challenge type: unknown');
142
+ });
143
+ });
144
+
145
+ describe('WASM Solver Path Integration (Mocked)', () => {
146
+ let mockWasmModule;
147
+
148
+ beforeEach(() => {
149
+ mockWasmModule = {
150
+ _malloc: vi.fn().mockImplementation(() => 1234), // Simule un pointeur mémoire
151
+ _free: vi.fn(),
152
+ HEAPU8: new Uint8Array(10000),
153
+ HEAP8: new Int8Array(10000),
154
+ _solve_cpu_target: vi.fn().mockReturnValue(1337),
155
+ _solve_memory_challenge: vi.fn().mockReturnValue(777)
156
+ };
157
+
158
+ // Injecter le module WASM globalement pour simuler le comportement du navigateur
159
+ global.window = {
160
+ wasmModule: mockWasmModule
161
+ };
162
+ });
163
+
164
+ afterEach(() => {
165
+ // Nettoyer l'environnement global après chaque test
166
+ delete global.window;
167
+ });
168
+
169
+ it('should redirect solveCpuTargetInline to WASM and manage heap memory safely', async () => {
170
+ const baseBlock = new Uint8Array([1, 2, 3, 4]);
171
+ const target = '0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF';
172
+
173
+ const solution = await solveCpuTargetInline(baseBlock, target);
174
+
175
+ // 1. Vérification du résultat natif retourné par le mock
176
+ expect(solution).toBe(1337);
177
+
178
+ // 2. Vérification des allocations et libérations mémoire pour le bloc de base et la cible hexadécimale
179
+ expect(mockWasmModule._malloc).toHaveBeenCalledTimes(2);
180
+ expect(mockWasmModule._free).toHaveBeenCalledTimes(2);
181
+
182
+ // 3. Vérification de l'appel à la fonction C++ exportée
183
+ expect(mockWasmModule._solve_cpu_target).toHaveBeenCalledWith(1234, baseBlock.length, 1234);
184
+ });
185
+
186
+ it('should redirect solveMemory to WASM and manage heap memory safely', async () => {
187
+ const seed = 'test-wasm-seed';
188
+ const difficulty = 4; // 4 MB
189
+
190
+ const solution = await solveMemory(seed, difficulty);
191
+
192
+ expect(solution).toBe(777);
193
+ expect(mockWasmModule._malloc).toHaveBeenCalledTimes(1);
194
+ expect(mockWasmModule._free).toHaveBeenCalledTimes(1);
195
+ expect(mockWasmModule._solve_memory_challenge).toHaveBeenCalledWith(1234, difficulty);
196
+ });
197
+ });
198
+ });