@anonympins/fingerprint 0.5.2 → 0.5.4
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 +18 -0
- package/package.json +1 -1
- package/src/js/fingerprint.client.js +1271 -1191
- package/src/js/fingerprint.js +202 -108
- 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/pow.solver.inline.js +500 -14
- package/src/js/pow.solver.js +323 -14
- package/src/js/problem-manager.js +1 -1
- package/src/js/redis-store.js +114 -42
- 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 +32 -2
- package/src/js/tests/ip-reputation.test.js +1 -1
- package/src/js/tests/pow.solver.test.js +5 -2
- package/src/js/tests/usefulWorkSolver.test.js +162 -0
- package/src/php/Challenge/ChallengeUtils.php +21 -8
- package/src/php/FingerprintClient.php +22 -5
- package/src/php/FingerprintEngine.php +232 -56
- 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
package/src/js/fingerprint.js
CHANGED
|
@@ -27,6 +27,57 @@ import {
|
|
|
27
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
28
|
const __dirname = dirname(__filename);
|
|
29
29
|
|
|
30
|
+
let dnsCircuitBreaker = {
|
|
31
|
+
state: 'CLOSED', // 'CLOSED', 'OPEN', 'HALF-OPEN'
|
|
32
|
+
failureCount: 0,
|
|
33
|
+
lastStateChange: 0,
|
|
34
|
+
threshold: 5,
|
|
35
|
+
cooldownMs: 30000, // 30 seconds
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function recordDnsSuccess() {
|
|
39
|
+
dnsCircuitBreaker.failureCount = 0;
|
|
40
|
+
dnsCircuitBreaker.state = 'CLOSED';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function recordDnsFailure() {
|
|
44
|
+
dnsCircuitBreaker.failureCount++;
|
|
45
|
+
if (dnsCircuitBreaker.failureCount >= dnsCircuitBreaker.threshold) {
|
|
46
|
+
dnsCircuitBreaker.state = 'OPEN';
|
|
47
|
+
dnsCircuitBreaker.lastStateChange = Date.now();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function canAttemptDns() {
|
|
52
|
+
if (dnsCircuitBreaker.state === 'CLOSED') {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (dnsCircuitBreaker.state === 'OPEN') {
|
|
56
|
+
if (Date.now() - dnsCircuitBreaker.lastStateChange > dnsCircuitBreaker.cooldownMs) {
|
|
57
|
+
dnsCircuitBreaker.state = 'HALF-OPEN';
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true; // HALF-OPEN
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const withTimeout = (promise, ms) => {
|
|
66
|
+
let timeoutId;
|
|
67
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
68
|
+
timeoutId = setTimeout(() => {
|
|
69
|
+
reject(new Error('DNS_TIMEOUT'));
|
|
70
|
+
}, ms);
|
|
71
|
+
});
|
|
72
|
+
return Promise.race([
|
|
73
|
+
promise.then((res) => {
|
|
74
|
+
clearTimeout(timeoutId);
|
|
75
|
+
return res;
|
|
76
|
+
}),
|
|
77
|
+
timeoutPromise
|
|
78
|
+
]);
|
|
79
|
+
};
|
|
80
|
+
|
|
30
81
|
export { createRedisStore } from "./redis-store.js";
|
|
31
82
|
export { createMongoDbStore } from "./mongodb-store.js";
|
|
32
83
|
|
|
@@ -1455,9 +1506,10 @@ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, o
|
|
|
1455
1506
|
}
|
|
1456
1507
|
|
|
1457
1508
|
function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
|
|
1458
|
-
const { nonce, sizeMb, queries, path } = challengeDetails;
|
|
1509
|
+
const { nonce, sizeMb, queries, path, peerId, peerBlockIdx } = challengeDetails;
|
|
1459
1510
|
const solverCode = getPowSolverCode();
|
|
1460
1511
|
const safePath = sanitizeRedirectPath(path);
|
|
1512
|
+
const coopTimeout = securityConfig?.pospace?.coopTimeout ?? 15;
|
|
1461
1513
|
|
|
1462
1514
|
const challengeScript = `
|
|
1463
1515
|
async function solve() {
|
|
@@ -1466,16 +1518,49 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
|
|
|
1466
1518
|
const clientSecret = ${safeJsonStringify(clientSecret)};
|
|
1467
1519
|
const queries = ${JSON.stringify(queries)};
|
|
1468
1520
|
const sizeMb = ${sizeMb};
|
|
1521
|
+
const nodeId = nonce;
|
|
1522
|
+
const peerId = ${safeJsonStringify(peerId || '')};
|
|
1523
|
+
const peerBlockIdx = ${peerBlockIdx ?? -1};
|
|
1524
|
+
const coopTimeout = ${coopTimeout};
|
|
1469
1525
|
|
|
1470
1526
|
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
1471
1527
|
await new Promise(r => setTimeout(r, 10));
|
|
1472
1528
|
|
|
1473
1529
|
try {
|
|
1474
1530
|
await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
|
|
1531
|
+
|
|
1532
|
+
if (peerId && peerBlockIdx !== -1) {
|
|
1533
|
+
// Enregistrement coopératif
|
|
1534
|
+
await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret));
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1475
1537
|
document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
|
|
1476
|
-
|
|
1538
|
+
|
|
1539
|
+
let peerBlock = "";
|
|
1540
|
+
if (peerId && peerBlockIdx !== -1) {
|
|
1541
|
+
document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
|
|
1542
|
+
const reqId = Math.random().toString(36).substring(2);
|
|
1543
|
+
await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId);
|
|
1544
|
+
|
|
1545
|
+
let attempts = 0;
|
|
1546
|
+
while (attempts < coopTimeout) {
|
|
1547
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId);
|
|
1548
|
+
const data = await res.json();
|
|
1549
|
+
if (data.status === 'ready') {
|
|
1550
|
+
peerBlock = data.block_data;
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1554
|
+
attempts++;
|
|
1555
|
+
}
|
|
1556
|
+
if (!peerBlock) {
|
|
1557
|
+
document.getElementById('loader').innerText = '⚠️ Peer de sous-réseau injoignable. Validation solo...';
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret, peerBlock);
|
|
1477
1562
|
|
|
1478
|
-
window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash;
|
|
1563
|
+
window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash + (peerBlock ? "&pow_coop=1" : "");
|
|
1479
1564
|
} catch(e) {
|
|
1480
1565
|
document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
|
|
1481
1566
|
}
|
|
@@ -2351,80 +2436,6 @@ function getClickVarianceScore(context) {
|
|
|
2351
2436
|
* @returns {string|null} The subnet CIDR or null if the IP is invalid.
|
|
2352
2437
|
*/
|
|
2353
2438
|
function getIpSubnet(ip, ipv4Prefix = 24, ipv6Prefix = 48) { // eslint-disable-line no-unused-vars
|
|
2354
|
-
/**
|
|
2355
|
-
* @private
|
|
2356
|
-
* Parses an IPv6 string, expanding '::' into a 16-byte Buffer.
|
|
2357
|
-
* @param {string} ipStr The IPv6 address string.
|
|
2358
|
-
* @returns {Buffer|null}
|
|
2359
|
-
*/
|
|
2360
|
-
const parseIPv6 = (ipStr) => {
|
|
2361
|
-
const parts = ipStr.split('::');
|
|
2362
|
-
if (parts.length > 2) return null;
|
|
2363
|
-
|
|
2364
|
-
let hextets = [];
|
|
2365
|
-
if (parts[0]) hextets.push(...parts[0].split(':'));
|
|
2366
|
-
|
|
2367
|
-
if (parts.length === 2) {
|
|
2368
|
-
const hextetsInPart2 = parts[1] ? parts[1].split(':').length : 0;
|
|
2369
|
-
const zerosToInsert = 8 - hextets.length - hextetsInPart2;
|
|
2370
|
-
for (let i = 0; i < zerosToInsert; i++) {
|
|
2371
|
-
hextets.push('0');
|
|
2372
|
-
}
|
|
2373
|
-
if (parts[1]) hextets.push(...parts[1].split(':'));
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
if (hextets.length !== 8) return null;
|
|
2377
|
-
|
|
2378
|
-
const buffer = Buffer.alloc(16);
|
|
2379
|
-
for (let i = 0; i < 8; i++) {
|
|
2380
|
-
const val = parseInt(hextets[i] || '0', 16);
|
|
2381
|
-
if (isNaN(val)) return null;
|
|
2382
|
-
buffer.writeUInt16BE(val, i * 2);
|
|
2383
|
-
}
|
|
2384
|
-
return buffer;
|
|
2385
|
-
};
|
|
2386
|
-
|
|
2387
|
-
/**
|
|
2388
|
-
* @private
|
|
2389
|
-
* Formats a 16-byte IPv6 buffer into a compressed string representation.
|
|
2390
|
-
* @param {Buffer} buffer The 16-byte buffer.
|
|
2391
|
-
* @returns {string}
|
|
2392
|
-
*/
|
|
2393
|
-
const formatIPv6 = (buffer) => {
|
|
2394
|
-
const hextets = [];
|
|
2395
|
-
for (let i = 0; i < 16; i += 2) {
|
|
2396
|
-
hextets.push(buffer.readUInt16BE(i).toString(16));
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
let bestStart = -1, bestLength = 0, currentStart = -1, currentLength = 0;
|
|
2400
|
-
for (let i = 0; i < hextets.length; i++) {
|
|
2401
|
-
if (hextets[i] === '0') {
|
|
2402
|
-
if (currentStart === -1) currentStart = i;
|
|
2403
|
-
currentLength++;
|
|
2404
|
-
} else {
|
|
2405
|
-
if (currentLength > bestLength) {
|
|
2406
|
-
bestStart = currentStart;
|
|
2407
|
-
bestLength = currentLength;
|
|
2408
|
-
}
|
|
2409
|
-
currentStart = -1;
|
|
2410
|
-
currentLength = 0;
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
if (currentLength > bestLength) {
|
|
2414
|
-
bestStart = currentStart;
|
|
2415
|
-
bestLength = currentLength;
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
// For subnet calculations, an uncompressed view is often clearer.
|
|
2419
|
-
// We will avoid compression to match test expectations.
|
|
2420
|
-
// if (bestLength > 1) {
|
|
2421
|
-
// const part1 = hextets.slice(0, bestStart).join(':');
|
|
2422
|
-
// const part2 = hextets.slice(bestStart + bestLength).join(':');
|
|
2423
|
-
// return `${part1}::${part2}`;
|
|
2424
|
-
// }
|
|
2425
|
-
return hextets.join(':');
|
|
2426
|
-
};
|
|
2427
|
-
|
|
2428
2439
|
try {
|
|
2429
2440
|
if (isIPv4(ip)) {
|
|
2430
2441
|
const ipBuffer = Buffer.from(ip.split('.').map(Number));
|
|
@@ -2433,14 +2444,37 @@ function getIpSubnet(ip, ipv4Prefix = 24, ipv6Prefix = 48) { // eslint-disable-l
|
|
|
2433
2444
|
for (let i = 0; i < 4; i++) ipBuffer[i] &= mask[i];
|
|
2434
2445
|
return `${Array.from(ipBuffer).join('.')}/${ipv4Prefix}`;
|
|
2435
2446
|
} else if (isIPv6(ip)) {
|
|
2436
|
-
|
|
2437
|
-
if (
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2447
|
+
let normalized = ip.trim().toLowerCase();
|
|
2448
|
+
if (normalized.includes("::")) {
|
|
2449
|
+
const parts = normalized.split("::");
|
|
2450
|
+
if (parts.length > 2) return null;
|
|
2451
|
+
const left = parts[0] ? parts[0].split(":") : [];
|
|
2452
|
+
const right = parts[1] ? parts[1].split(":") : [];
|
|
2453
|
+
const missing = 8 - (left.length + right.length);
|
|
2454
|
+
const middle = Array(missing).fill("0000");
|
|
2455
|
+
normalized = [...left, ...middle, ...right].join(":");
|
|
2456
|
+
} else {
|
|
2457
|
+
const parts = normalized.split(":");
|
|
2458
|
+
if (parts.length !== 8) return null;
|
|
2459
|
+
}
|
|
2460
|
+
const groups = normalized.split(":").map(g => {
|
|
2461
|
+
const val = parseInt(g, 16);
|
|
2462
|
+
return isNaN(val) ? "0000" : val.toString(16).padStart(4, "0");
|
|
2463
|
+
});
|
|
2464
|
+
for (let i = 0; i < 8; i++) {
|
|
2465
|
+
const startBit = i * 16;
|
|
2466
|
+
if (ipv6Prefix >= (i + 1) * 16) {
|
|
2467
|
+
continue;
|
|
2468
|
+
} else if (ipv6Prefix <= startBit) {
|
|
2469
|
+
groups[i] = "0000";
|
|
2470
|
+
} else {
|
|
2471
|
+
const bitsToKeep = ipv6Prefix - startBit;
|
|
2472
|
+
const val = parseInt(groups[i], 16);
|
|
2473
|
+
const mask = (0xffff << (16 - bitsToKeep)) & 0xffff;
|
|
2474
|
+
groups[i] = (val & mask).toString(16).padStart(4, "0");
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
return `${groups.join(":")}/${ipv6Prefix}`;
|
|
2444
2478
|
}
|
|
2445
2479
|
} catch (e) {
|
|
2446
2480
|
// Catch any unexpected errors during parsing or manipulation
|
|
@@ -2853,12 +2887,19 @@ async function resolveRequestIdentity(context, securityConfig = {}) {
|
|
|
2853
2887
|
// Case 2: New user or lost/invalid cookie.
|
|
2854
2888
|
deviceId = crypto.randomUUID(); // Generate a new "passport".
|
|
2855
2889
|
|
|
2890
|
+
const isHttps = context.headers?.['x-forwarded-proto'] === 'https' ||
|
|
2891
|
+
context.rawReq?.secure ||
|
|
2892
|
+
context.rawReq?.protocol === 'https' ||
|
|
2893
|
+
context.rawReq?.connection?.encrypted;
|
|
2894
|
+
const secureOption = isHttps || process.env.NODE_ENV === "production";
|
|
2895
|
+
|
|
2856
2896
|
// Return the intention to set a cookie.
|
|
2857
2897
|
newCookie = {
|
|
2858
2898
|
name: "device_id",
|
|
2859
2899
|
value: deviceId,
|
|
2860
2900
|
options: {
|
|
2861
|
-
httpOnly: true, secure:
|
|
2901
|
+
httpOnly: true, secure: secureOption, sameSite: "strict",
|
|
2902
|
+
...(secureOption && { partitioned: true }),
|
|
2862
2903
|
// Le maxAge est maintenant configurable. Par défaut, c'est un cookie de session.
|
|
2863
2904
|
...(securityConfig.deviceIdCookieMaxAge && { maxAge: securityConfig.deviceIdCookieMaxAge }),
|
|
2864
2905
|
}
|
|
@@ -3250,7 +3291,7 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
3250
3291
|
}
|
|
3251
3292
|
|
|
3252
3293
|
// Redirect with both solutions and the fingerprint used to solve.
|
|
3253
|
-
const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
3294
|
+
const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + encodeURIComponent(JSON.stringify(memSolution));
|
|
3254
3295
|
window.location.href = finalUrl;
|
|
3255
3296
|
}
|
|
3256
3297
|
|
|
@@ -3705,6 +3746,10 @@ export class FingerprintEngine {
|
|
|
3705
3746
|
return false;
|
|
3706
3747
|
}
|
|
3707
3748
|
|
|
3749
|
+
if (!canAttemptDns()) {
|
|
3750
|
+
return false;
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3708
3753
|
const cacheKey = `ip-whitelist:${clientIp}`;
|
|
3709
3754
|
const cachedStatus = await store.get(cacheKey);
|
|
3710
3755
|
|
|
@@ -3717,32 +3762,35 @@ export class FingerprintEngine {
|
|
|
3717
3762
|
|
|
3718
3763
|
try {
|
|
3719
3764
|
// 1. Reverse DNS lookup
|
|
3720
|
-
const hostnames = await dns.reverse(clientIp);
|
|
3765
|
+
const hostnames = await withTimeout(dns.reverse(clientIp), 500);
|
|
3721
3766
|
const validHostname = hostnames.find(h => h.endsWith(matchedRule.hostnameSuffix));
|
|
3722
3767
|
|
|
3723
3768
|
if (!validHostname) {
|
|
3724
|
-
await store.set(cacheKey, 'failed',
|
|
3769
|
+
await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
|
|
3725
3770
|
return false;
|
|
3726
3771
|
}
|
|
3727
3772
|
|
|
3728
3773
|
// 2. Forward DNS lookup
|
|
3729
3774
|
let addresses = [];
|
|
3730
3775
|
try {
|
|
3731
|
-
|
|
3776
|
+
addresses = await withTimeout(dns.resolve(validHostname), 500);
|
|
3732
3777
|
} catch (e) {}
|
|
3733
3778
|
try {
|
|
3734
|
-
|
|
3779
|
+
const ipv6 = await withTimeout(dns.resolve(validHostname, 'AAAA'), 500);
|
|
3735
3780
|
addresses = addresses.concat(ipv6);
|
|
3736
3781
|
} catch (e) {}
|
|
3737
3782
|
if (addresses.includes(clientIp)) {
|
|
3783
|
+
recordDnsSuccess();
|
|
3738
3784
|
await store.set(cacheKey, 'verified', 86400); // Cache success for 24h (TTL in seconds)
|
|
3739
3785
|
return true;
|
|
3740
3786
|
}
|
|
3741
3787
|
} catch (error) {
|
|
3742
|
-
|
|
3788
|
+
recordDnsFailure();
|
|
3789
|
+
await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
|
|
3790
|
+
return false;
|
|
3743
3791
|
}
|
|
3744
3792
|
|
|
3745
|
-
await store.set(cacheKey, 'failed',
|
|
3793
|
+
await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
|
|
3746
3794
|
return false;
|
|
3747
3795
|
}
|
|
3748
3796
|
|
|
@@ -4040,6 +4088,11 @@ export class FingerprintEngine {
|
|
|
4040
4088
|
const finalQueryString = finalSearchParams.toString();
|
|
4041
4089
|
const finalRedirectPath = finalQueryString ? `${originalUrl.pathname}?${finalQueryString}` : originalUrl.pathname;
|
|
4042
4090
|
this._log('Redirecting to clean path', { finalRedirectPath, cookieMaxAge: finalTtl });
|
|
4091
|
+
const isHttps = requestContext.headers?.['x-forwarded-proto'] === 'https' ||
|
|
4092
|
+
requestContext.rawReq?.secure ||
|
|
4093
|
+
requestContext.rawReq?.protocol === 'https' ||
|
|
4094
|
+
requestContext.rawReq?.connection?.encrypted;
|
|
4095
|
+
const secureOption = isHttps || this.isProduction;
|
|
4043
4096
|
return {
|
|
4044
4097
|
action: 'redirect',
|
|
4045
4098
|
path: finalRedirectPath,
|
|
@@ -4048,7 +4101,13 @@ export class FingerprintEngine {
|
|
|
4048
4101
|
cookie: {
|
|
4049
4102
|
name: 'pow_clearance',
|
|
4050
4103
|
value: ticket, // The ticket itself
|
|
4051
|
-
options: {
|
|
4104
|
+
options: {
|
|
4105
|
+
httpOnly: true,
|
|
4106
|
+
secure: secureOption,
|
|
4107
|
+
sameSite: 'strict',
|
|
4108
|
+
...(secureOption && { partitioned: true }),
|
|
4109
|
+
maxAge: finalTtl
|
|
4110
|
+
} // Options for setting the cookie
|
|
4052
4111
|
}
|
|
4053
4112
|
};
|
|
4054
4113
|
} else {
|
|
@@ -4123,7 +4182,7 @@ export class FingerprintEngine {
|
|
|
4123
4182
|
if (challengeContext) {
|
|
4124
4183
|
try {
|
|
4125
4184
|
const workResult = JSON.parse(pow_solution_work_result);
|
|
4126
|
-
const defaultPath = resolve(__dirname, '..', '..', 'problems.config.json');
|
|
4185
|
+
const defaultPath = resolve(__dirname, '..', '..', 'config', 'problems.config.json');
|
|
4127
4186
|
const configPath = this.securityConfig.usefulWorkConfigPath || (existsSync(defaultPath) ? defaultPath : undefined);
|
|
4128
4187
|
const manager = await getProblemManager({
|
|
4129
4188
|
configPath,
|
|
@@ -4319,7 +4378,7 @@ export class FingerprintEngine {
|
|
|
4319
4378
|
this._log('Issuing a useful work challenge', { finalScore });
|
|
4320
4379
|
|
|
4321
4380
|
try {
|
|
4322
|
-
const defaultPath = resolve(process.cwd(), 'problems.config.json');
|
|
4381
|
+
const defaultPath = resolve(process.cwd(), 'config', 'problems.config.json');
|
|
4323
4382
|
const configPath = this.securityConfig.usefulWorkConfigPath || (existsSync(defaultPath) ? defaultPath : undefined);
|
|
4324
4383
|
const manager = await getProblemManager({
|
|
4325
4384
|
configPath,
|
|
@@ -5285,7 +5344,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5285
5344
|
|
|
5286
5345
|
// Initialize the problem manager with the configured path, if provided.
|
|
5287
5346
|
if (securityConfig.enableUsefulWork) {
|
|
5288
|
-
const defaultPath = resolve(__dirname, '..', '..', 'problems.config.json');
|
|
5347
|
+
const defaultPath = resolve(__dirname, '..', '..', 'config', 'problems.config.json');
|
|
5289
5348
|
const configPath = securityConfig.usefulWorkConfigPath || (existsSync(defaultPath) ? defaultPath : undefined);
|
|
5290
5349
|
getProblemManager({
|
|
5291
5350
|
configPath,
|
|
@@ -5505,6 +5564,10 @@ export const __internal = {
|
|
|
5505
5564
|
getTcpAnomalyScore, // Expose for testing,
|
|
5506
5565
|
getQuicAnomalyScore, // NOUVEAU: Expose pour les tests
|
|
5507
5566
|
getRenderingAnomalyScore, // NOUVEAU: Expose pour les tests
|
|
5567
|
+
dnsCircuitBreaker,
|
|
5568
|
+
recordDnsSuccess,
|
|
5569
|
+
recordDnsFailure,
|
|
5570
|
+
canAttemptDns,
|
|
5508
5571
|
registerCooperativeNode,
|
|
5509
5572
|
findPeerInSubnet,
|
|
5510
5573
|
handleCooperativeRequest
|
|
@@ -5526,48 +5589,79 @@ export function sanitizeTrafficData(trafficData) {
|
|
|
5526
5589
|
if (!trafficData || trafficData.length === 0) {
|
|
5527
5590
|
return [];
|
|
5528
5591
|
}
|
|
5529
|
-
const
|
|
5592
|
+
const suspiciousLogs = [];
|
|
5593
|
+
const passedLogs = [];
|
|
5530
5594
|
const deviceCounts = new Map();
|
|
5531
5595
|
const ipCounts = new Map();
|
|
5532
5596
|
const subnetCounts = new Map();
|
|
5597
|
+
const hardwareClusterCounts = new Map();
|
|
5598
|
+
const hwClusterCache = new Map();
|
|
5533
5599
|
|
|
5534
5600
|
// Calcul des quotas maximums pour éviter l'influence démesurée d'une entité
|
|
5535
5601
|
const maxLogsPerDevice = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% contribution per device
|
|
5536
5602
|
const maxLogsPerIp = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% par adresse IP individuelle
|
|
5537
5603
|
const maxLogsPerSubnet = Math.max(5, Math.floor(trafficData.length * 0.05)); // Max 5% par bloc réseau (anti-proxy-rotation)
|
|
5604
|
+
const maxLogsPerHardwareCluster = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% par cluster matériel stable
|
|
5605
|
+
|
|
5606
|
+
const getHardwareCluster = (log) => {
|
|
5607
|
+
const fp = log.deviceHash || log.fingerprint || log.deviceFingerprint || '';
|
|
5608
|
+
if (!fp || typeof fp !== 'string') {
|
|
5609
|
+
return log.deviceId || 'anonymous-cluster';
|
|
5610
|
+
}
|
|
5611
|
+
if (hwClusterCache.has(fp)) {
|
|
5612
|
+
return hwClusterCache.get(fp);
|
|
5613
|
+
}
|
|
5614
|
+
const parts = fp.split('|');
|
|
5615
|
+
const hwComponents = [];
|
|
5616
|
+
for (const part of parts) {
|
|
5617
|
+
const pair = part.split(':');
|
|
5618
|
+
if (pair.length === 2 && (pair[0] === 'gpu' || pair[0] === 'cvs' || pair[0] === 'hw')) {
|
|
5619
|
+
hwComponents.push(part);
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
const result = hwComponents.length > 0 ? hwComponents.sort().join('|') : (log.deviceId || 'anonymous-cluster');
|
|
5623
|
+
hwClusterCache.set(fp, result);
|
|
5624
|
+
return result;
|
|
5625
|
+
};
|
|
5538
5626
|
|
|
5539
5627
|
for (const log of trafficData) {
|
|
5540
5628
|
const devId = log.deviceId || 'anonymous';
|
|
5541
5629
|
const ip = log.clientIp || log.ip || 'unknown';
|
|
5542
5630
|
const subnet = getIpSubnet(ip) || 'unknown-subnet';
|
|
5631
|
+
const hwCluster = getHardwareCluster(log);
|
|
5543
5632
|
|
|
5544
5633
|
const currentDeviceCount = deviceCounts.get(devId) || 0;
|
|
5545
5634
|
const currentIpCount = ipCounts.get(ip) || 0;
|
|
5546
5635
|
const currentSubnetCount = subnetCounts.get(subnet) || 0;
|
|
5636
|
+
const currentHwClusterCount = hardwareClusterCounts.get(hwCluster) || 0;
|
|
5547
5637
|
|
|
5548
|
-
// Filtrage anti-poisoning strict sur
|
|
5638
|
+
// Filtrage anti-poisoning strict sur 4 axes cumulatifs (incluant le clustering matériel stable)
|
|
5549
5639
|
if (
|
|
5550
5640
|
currentDeviceCount < maxLogsPerDevice &&
|
|
5551
5641
|
(ip === 'unknown' || currentIpCount < maxLogsPerIp) &&
|
|
5552
|
-
(subnet === 'unknown-subnet' || currentSubnetCount < maxLogsPerSubnet)
|
|
5642
|
+
(subnet === 'unknown-subnet' || currentSubnetCount < maxLogsPerSubnet) &&
|
|
5643
|
+
currentHwClusterCount < maxLogsPerHardwareCluster
|
|
5553
5644
|
) {
|
|
5554
5645
|
deviceCounts.set(devId, currentDeviceCount + 1);
|
|
5555
5646
|
if (ip !== 'unknown') ipCounts.set(ip, currentIpCount + 1);
|
|
5556
5647
|
if (subnet !== 'unknown-subnet') subnetCounts.set(subnet, currentSubnetCount + 1);
|
|
5648
|
+
hardwareClusterCounts.set(hwCluster, currentHwClusterCount + 1);
|
|
5557
5649
|
|
|
5558
|
-
|
|
5650
|
+
if (log.type === 'request_passed') {
|
|
5651
|
+
passedLogs.push(log);
|
|
5652
|
+
} else {
|
|
5653
|
+
suspiciousLogs.push(log);
|
|
5654
|
+
}
|
|
5559
5655
|
}
|
|
5560
5656
|
}
|
|
5561
5657
|
|
|
5562
|
-
const passedLogs = tempSanitized.filter(log => log.type === 'request_passed');
|
|
5563
|
-
const suspiciousLogs = tempSanitized.filter(log => log.type !== 'request_passed');
|
|
5564
|
-
|
|
5565
5658
|
const minDataPoints = 200; // Seuil par défaut
|
|
5566
5659
|
const maxPassedAllowed = Math.max(minDataPoints, suspiciousLogs.length * 9);
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5660
|
+
if (passedLogs.length > maxPassedAllowed) {
|
|
5661
|
+
const shuffledPassed = passedLogs.sort(() => 0.5 - Math.random());
|
|
5662
|
+
return [...suspiciousLogs, ...shuffledPassed.slice(0, maxPassedAllowed)];
|
|
5663
|
+
}
|
|
5664
|
+
return [...suspiciousLogs, ...passedLogs];
|
|
5571
5665
|
}
|
|
5572
5666
|
/**
|
|
5573
5667
|
* Assainit et limite la taille/ancienneté des données de trafic pour éviter les fuites de mémoire.
|