@anonympins/fingerprint 0.5.1 → 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 +27 -0
- package/README.md +118 -115
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +52 -13
- package/src/js/fingerprint.builder.js +169 -168
- package/src/js/fingerprint.client.js +1271 -1189
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +436 -162
- package/src/js/fingerprint.utils.js +30 -0
- package/src/js/gpu_pow.solver.js +96 -1
- package/src/js/library.js +40 -21
- package/src/js/mongodb-store.js +165 -79
- package/src/js/pow.solver.inline.js +627 -30
- package/src/js/pow.solver.js +453 -31
- 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.engine.test.js +370 -370
- package/src/js/tests/fingerprint.test.js +119 -5
- package/src/js/tests/gpu_pow.test.js +81 -0
- package/src/js/tests/ip-reputation.test.js +1 -1
- package/src/js/tests/pow.solver.test.js +9 -6
- package/src/js/tests/usefulWorkSolver.test.js +162 -0
- package/src/php/Challenge/ChallengeUtils.php +137 -35
- package/src/php/FingerprintClient.php +22 -5
- package/src/php/FingerprintEngine.php +335 -58
- 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
|
@@ -8,12 +8,76 @@ import {DynamicWasmGenerator} from "./dynamic-wasm.js";
|
|
|
8
8
|
import {readFileSync, existsSync} from "node:fs";
|
|
9
9
|
import {fileURLToPath} from "node:url";
|
|
10
10
|
import {dirname, join, resolve} from "node:path";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
verifyZkpProof,
|
|
13
|
+
sanitizeRedirectPath,
|
|
14
|
+
decodePolymorphicFingerprint,
|
|
15
|
+
deepMerge,
|
|
16
|
+
getHeaderSignature,
|
|
17
|
+
parseJa3,
|
|
18
|
+
modPow,
|
|
19
|
+
hashNetwork,
|
|
20
|
+
normalizeReferer,
|
|
21
|
+
isPrivateIp,
|
|
22
|
+
parseUserAgent,
|
|
23
|
+
safeJsonStringify
|
|
24
|
+
} from "./fingerprint.utils.js";
|
|
12
25
|
|
|
13
26
|
|
|
14
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
28
|
const __dirname = dirname(__filename);
|
|
16
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
|
+
|
|
17
81
|
export { createRedisStore } from "./redis-store.js";
|
|
18
82
|
export { createMongoDbStore } from "./mongodb-store.js";
|
|
19
83
|
|
|
@@ -22,6 +86,42 @@ let lastMappingTime = 0;
|
|
|
22
86
|
let isCompilingMapping = false;
|
|
23
87
|
const MAPPING_ROTATION_INTERVAL = 60000; // 60 seconds
|
|
24
88
|
|
|
89
|
+
const configDir = resolve(__dirname, '../../config');
|
|
90
|
+
|
|
91
|
+
const loadBotWhitelist = (filename, fallbackEntries) => {
|
|
92
|
+
const filePath = join(configDir, filename);
|
|
93
|
+
if (existsSync(filePath)) {
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error(`[Fingerprint] Error loading whitelist file ${filename}:`, e.message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return fallbackEntries;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const googlebotEntries = loadBotWhitelist('googlebot.json', [
|
|
104
|
+
"2001:4860:4801:10::/64",
|
|
105
|
+
"2001:4860:4801:11::/64",
|
|
106
|
+
"2001:4860:4801:12::/64",
|
|
107
|
+
// ... [Keep fallback inline values for safety]
|
|
108
|
+
"66.249.79.64"
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const bingbotEntries = loadBotWhitelist('bingbot.json', [
|
|
112
|
+
"157.55.39.0/24",
|
|
113
|
+
"207.46.13.0/24",
|
|
114
|
+
// ... [Keep fallback inline values for safety]
|
|
115
|
+
"40.77.178.0/23"
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const yandexEntries = loadBotWhitelist('yandex.json', [
|
|
119
|
+
"2a02:6b8::/29",
|
|
120
|
+
"5.45.192.0/18",
|
|
121
|
+
// ... [Keep fallback inline values for safety]
|
|
122
|
+
"213.180.192.0/19"
|
|
123
|
+
]);
|
|
124
|
+
|
|
25
125
|
function generateSessionMapping() {
|
|
26
126
|
const randomStr = (len = 6) => crypto.randomBytes(len).toString('hex').replace(/[0-9]/g, 'g').substring(0, len);
|
|
27
127
|
const randomHeader = () => `X-Sess-${crypto.randomBytes(4).toString('hex')}`;
|
|
@@ -584,15 +684,17 @@ const getPowSecret = () => {
|
|
|
584
684
|
return secret || "fallback-dev-secret-32-chars-minimum";
|
|
585
685
|
};
|
|
586
686
|
|
|
687
|
+
let cachedPowSolverCode = null;
|
|
587
688
|
/**
|
|
588
689
|
* Loads the pow.solver.js content for inlining in HTML pages.
|
|
589
690
|
* @returns {string} The solver JavaScript code.
|
|
590
691
|
*/
|
|
591
692
|
const getPowSolverCode = () => {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
693
|
+
if (!cachedPowSolverCode) {
|
|
694
|
+
const solverPath = join(__dirname, 'pow.solver.inline.js'); // Utilise la version inline
|
|
695
|
+
cachedPowSolverCode = readFileSync(solverPath, 'utf-8');
|
|
696
|
+
}
|
|
697
|
+
return cachedPowSolverCode;
|
|
596
698
|
};
|
|
597
699
|
/**
|
|
598
700
|
* Extracts the "stable" part of a fingerprint string.
|
|
@@ -858,6 +960,7 @@ const generateTspChallenge = (
|
|
|
858
960
|
) => {
|
|
859
961
|
const citiesJson = JSON.stringify(cities);
|
|
860
962
|
const solverCode = getPowSolverCode();
|
|
963
|
+
const safePath = sanitizeRedirectPath(path);
|
|
861
964
|
return `
|
|
862
965
|
<html>
|
|
863
966
|
<head><title>Advanced Security Check (Level 3)</title></head>
|
|
@@ -868,14 +971,14 @@ const generateTspChallenge = (
|
|
|
868
971
|
<script>${solverCode}</script>
|
|
869
972
|
<script>
|
|
870
973
|
const cities = ${citiesJson}; // Safe, as it's JSON
|
|
871
|
-
const nonce = ${
|
|
974
|
+
const nonce = ${safeJsonStringify(nonce)}; // Safe
|
|
872
975
|
const targetMaxDistance = ${targetMaxDistance};
|
|
873
976
|
|
|
874
977
|
async function solve() {
|
|
875
978
|
const result = await window.solveTspChallenge(cities, targetMaxDistance);
|
|
876
979
|
|
|
877
980
|
if (result.distance <= targetMaxDistance) {
|
|
878
|
-
window.location.href = ${
|
|
981
|
+
window.location.href = ${safeJsonStringify(safePath)} + "?pow_type=tsp&pow_nonce=" + nonce + "&pow_solution=" + JSON.stringify(result.path);
|
|
879
982
|
} else {
|
|
880
983
|
document.getElementById('loader').innerText = "Error: Could not find a sufficient solution. Please try again.";
|
|
881
984
|
}
|
|
@@ -972,6 +1075,7 @@ const generateMemoryPoWChallenge = (
|
|
|
972
1075
|
difficulty = 16,
|
|
973
1076
|
path = "",
|
|
974
1077
|
) => {
|
|
1078
|
+
const safePath = sanitizeRedirectPath(path);
|
|
975
1079
|
// difficulty here is the buffer size in MB.
|
|
976
1080
|
return `
|
|
977
1081
|
<html>
|
|
@@ -982,8 +1086,8 @@ const generateMemoryPoWChallenge = (
|
|
|
982
1086
|
<div id="loader" style="margin:20px;">⚙️ Performing memory allocation and calculation... (${difficulty} MB)</div>
|
|
983
1087
|
<script>
|
|
984
1088
|
async function solve() {
|
|
985
|
-
const nonce =
|
|
986
|
-
|
|
1089
|
+
const nonce = ${safeJsonStringify(nonce)};
|
|
1090
|
+
const size = ${difficulty} * 1024 * 1024; // en octets
|
|
987
1091
|
const iterations = size / 16;
|
|
988
1092
|
|
|
989
1093
|
try {
|
|
@@ -1000,8 +1104,8 @@ const generateMemoryPoWChallenge = (
|
|
|
1000
1104
|
}
|
|
1001
1105
|
window.location.href = "${path}" + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
|
|
1002
1106
|
} catch(e) {
|
|
1003
|
-
|
|
1004
|
-
|
|
1107
|
+
window.location.href = ${safeJsonStringify(safePath)} + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
|
|
1108
|
+
}
|
|
1005
1109
|
}
|
|
1006
1110
|
solve();
|
|
1007
1111
|
</script>
|
|
@@ -1056,40 +1160,123 @@ export const verifyPoWAndGenerateTicket = async (
|
|
|
1056
1160
|
* For higher difficulties (production workloads), we skip the massive memory allocation on the server,
|
|
1057
1161
|
* avoiding server-side memory DoS vectors completely.
|
|
1058
1162
|
*/
|
|
1163
|
+
function getChallengedIndices(seed, solution, numBlocks, k = 4) {
|
|
1164
|
+
const indices = [];
|
|
1165
|
+
let h = cyrb53(seed + ":" + solution);
|
|
1166
|
+
for (let i = 0; i < k; i++) {
|
|
1167
|
+
h = Math.imul(h ^ i, 1597334677);
|
|
1168
|
+
indices.push(Math.abs(h) % numBlocks);
|
|
1169
|
+
}
|
|
1170
|
+
return indices;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function verifyMerkleProof(leafHash, index, proof, root) {
|
|
1174
|
+
let currentHash = leafHash;
|
|
1175
|
+
let idx = index;
|
|
1176
|
+
for (let i = 0; i < proof.length; i++) {
|
|
1177
|
+
const sibling = proof[i];
|
|
1178
|
+
const combined = idx % 2 === 0 ? currentHash + sibling : sibling + currentHash;
|
|
1179
|
+
currentHash = crypto.createHash('sha256').update(Buffer.from(combined, 'hex')).digest('hex');
|
|
1180
|
+
idx = Math.floor(idx / 2);
|
|
1181
|
+
}
|
|
1182
|
+
return currentHash === root;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function verifyMemoryPoWLegacy(nonce, solution, difficulty, clientSecret) {
|
|
1186
|
+
const size = difficulty * 1024 * 1024;
|
|
1187
|
+
const iterations = size / 16;
|
|
1188
|
+
const buffer = new Uint32Array(size / 4);
|
|
1189
|
+
const seed = `:${nonce}:${clientSecret}`;
|
|
1190
|
+
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
1191
|
+
|
|
1192
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
1193
|
+
buffer[i] = h = Math.imul(h ^ i, 1597334677);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
let finalHash = 0;
|
|
1197
|
+
let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
|
|
1198
|
+
for (let i = 0; i < iterations; i++) {
|
|
1199
|
+
addr = buffer[addr] % buffer.length;
|
|
1200
|
+
finalHash ^= addr;
|
|
1201
|
+
}
|
|
1202
|
+
return finalHash === solution;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1059
1205
|
export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret = '') => {
|
|
1060
1206
|
const MAX_ALLOWED_MEM_DIFFICULTY = 128; // 128MB
|
|
1061
1207
|
if (difficulty > MAX_ALLOWED_MEM_DIFFICULTY) {
|
|
1062
1208
|
console.warn(`[Security] Memory PoW verification attempt with excessive difficulty: ${difficulty}MB. Denied.`);
|
|
1063
1209
|
return false;
|
|
1064
1210
|
}
|
|
1211
|
+
if (Number(difficulty) === 0) {
|
|
1212
|
+
return true;
|
|
1213
|
+
}
|
|
1065
1214
|
if (!solution) {
|
|
1066
1215
|
return false;
|
|
1067
1216
|
}
|
|
1068
1217
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1218
|
+
let data;
|
|
1219
|
+
try {
|
|
1220
|
+
data = typeof solution === 'string' ? JSON.parse(solution) : solution;
|
|
1221
|
+
} catch (e) {
|
|
1222
|
+
data = null;
|
|
1073
1223
|
}
|
|
1074
1224
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1225
|
+
if (!data || typeof data !== 'object' || data.solution === undefined || !data.merkleRoot || !data.proofs) {
|
|
1226
|
+
if (difficulty <= 4 && /^\d+$/.test(String(solution))) {
|
|
1227
|
+
return verifyMemoryPoWLegacy(nonce, parseInt(solution, 10), difficulty, clientSecret);
|
|
1228
|
+
}
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const { solution: sol, merkleRoot, proofs } = data;
|
|
1233
|
+
const numBlocks = difficulty * 256;
|
|
1079
1234
|
const seed = `:${nonce}:${clientSecret}`;
|
|
1080
|
-
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
1081
1235
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1236
|
+
const challengedIndices = getChallengedIndices(seed, sol, numBlocks, 4);
|
|
1237
|
+
|
|
1238
|
+
for (const b of challengedIndices) {
|
|
1239
|
+
const proof = proofs[b] || proofs[String(b)];
|
|
1240
|
+
if (!proof) return false;
|
|
1241
|
+
|
|
1242
|
+
const block = new Uint32Array(1024);
|
|
1243
|
+
let h = cyrb53(seed + ":" + b);
|
|
1244
|
+
for (let i = 0; i < 1024; i++) {
|
|
1245
|
+
block[i] = (h = Math.imul(h ^ i, 1597334677));
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const expectedLeaf = crypto.createHash('sha256').update(Buffer.from(block.buffer)).digest('hex');
|
|
1249
|
+
|
|
1250
|
+
if (!verifyMerkleProof(expectedLeaf, b, proof, merkleRoot)) {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1084
1253
|
}
|
|
1085
1254
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1255
|
+
const blockCache = new Map();
|
|
1256
|
+
function getBlockElement(blockIdx, elementIdx) {
|
|
1257
|
+
if (!blockCache.has(blockIdx)) {
|
|
1258
|
+
const block = new Uint32Array(1024);
|
|
1259
|
+
let h = cyrb53(seed + ":" + blockIdx);
|
|
1260
|
+
for (let i = 0; i < 1024; i++) {
|
|
1261
|
+
block[i] = (h = Math.imul(h ^ i, 1597334677));
|
|
1262
|
+
}
|
|
1263
|
+
blockCache.set(blockIdx, block);
|
|
1264
|
+
}
|
|
1265
|
+
return blockCache.get(blockIdx)[elementIdx];
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const totalElements = numBlocks * 1024;
|
|
1269
|
+
let addr = totalElements > 0 ? getBlockElement(0, 0) % totalElements : 0;
|
|
1270
|
+
let expectedSolution = 0;
|
|
1271
|
+
const iterations = 1024;
|
|
1088
1272
|
for (let i = 0; i < iterations; i++) {
|
|
1089
|
-
|
|
1090
|
-
|
|
1273
|
+
const blockIdx = Math.floor(addr / 1024);
|
|
1274
|
+
const elementIdx = addr % 1024;
|
|
1275
|
+
addr = getBlockElement(blockIdx, elementIdx) % totalElements;
|
|
1276
|
+
expectedSolution ^= addr;
|
|
1091
1277
|
}
|
|
1092
|
-
|
|
1278
|
+
|
|
1279
|
+
return expectedSolution === parseInt(sol, 10);
|
|
1093
1280
|
};
|
|
1094
1281
|
|
|
1095
1282
|
export async function verifySpacePoW(nonce, solution, queries, seed, clientSecret) {
|
|
@@ -1319,26 +1506,61 @@ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, o
|
|
|
1319
1506
|
}
|
|
1320
1507
|
|
|
1321
1508
|
function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
|
|
1322
|
-
const { nonce, sizeMb, queries, path } = challengeDetails;
|
|
1509
|
+
const { nonce, sizeMb, queries, path, peerId, peerBlockIdx } = challengeDetails;
|
|
1323
1510
|
const solverCode = getPowSolverCode();
|
|
1324
|
-
|
|
1325
|
-
const
|
|
1511
|
+
const safePath = sanitizeRedirectPath(path);
|
|
1512
|
+
const coopTimeout = securityConfig?.pospace?.coopTimeout ?? 15;
|
|
1513
|
+
|
|
1514
|
+
const challengeScript = `
|
|
1326
1515
|
async function solve() {
|
|
1327
|
-
const nonce = ${
|
|
1328
|
-
|
|
1329
|
-
|
|
1516
|
+
const nonce = ${safeJsonStringify(nonce)};
|
|
1517
|
+
const path = ${safeJsonStringify(safePath)};
|
|
1518
|
+
const clientSecret = ${safeJsonStringify(clientSecret)};
|
|
1330
1519
|
const queries = ${JSON.stringify(queries)};
|
|
1331
1520
|
const sizeMb = ${sizeMb};
|
|
1521
|
+
const nodeId = nonce;
|
|
1522
|
+
const peerId = ${safeJsonStringify(peerId || '')};
|
|
1523
|
+
const peerBlockIdx = ${peerBlockIdx ?? -1};
|
|
1524
|
+
const coopTimeout = ${coopTimeout};
|
|
1332
1525
|
|
|
1333
1526
|
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
1334
1527
|
await new Promise(r => setTimeout(r, 10));
|
|
1335
1528
|
|
|
1336
1529
|
try {
|
|
1337
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
|
+
|
|
1338
1537
|
document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
|
|
1339
|
-
|
|
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);
|
|
1340
1562
|
|
|
1341
|
-
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" : "");
|
|
1342
1564
|
} catch(e) {
|
|
1343
1565
|
document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
|
|
1344
1566
|
}
|
|
@@ -2214,80 +2436,6 @@ function getClickVarianceScore(context) {
|
|
|
2214
2436
|
* @returns {string|null} The subnet CIDR or null if the IP is invalid.
|
|
2215
2437
|
*/
|
|
2216
2438
|
function getIpSubnet(ip, ipv4Prefix = 24, ipv6Prefix = 48) { // eslint-disable-line no-unused-vars
|
|
2217
|
-
/**
|
|
2218
|
-
* @private
|
|
2219
|
-
* Parses an IPv6 string, expanding '::' into a 16-byte Buffer.
|
|
2220
|
-
* @param {string} ipStr The IPv6 address string.
|
|
2221
|
-
* @returns {Buffer|null}
|
|
2222
|
-
*/
|
|
2223
|
-
const parseIPv6 = (ipStr) => {
|
|
2224
|
-
const parts = ipStr.split('::');
|
|
2225
|
-
if (parts.length > 2) return null;
|
|
2226
|
-
|
|
2227
|
-
let hextets = [];
|
|
2228
|
-
if (parts[0]) hextets.push(...parts[0].split(':'));
|
|
2229
|
-
|
|
2230
|
-
if (parts.length === 2) {
|
|
2231
|
-
const hextetsInPart2 = parts[1] ? parts[1].split(':').length : 0;
|
|
2232
|
-
const zerosToInsert = 8 - hextets.length - hextetsInPart2;
|
|
2233
|
-
for (let i = 0; i < zerosToInsert; i++) {
|
|
2234
|
-
hextets.push('0');
|
|
2235
|
-
}
|
|
2236
|
-
if (parts[1]) hextets.push(...parts[1].split(':'));
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
if (hextets.length !== 8) return null;
|
|
2240
|
-
|
|
2241
|
-
const buffer = Buffer.alloc(16);
|
|
2242
|
-
for (let i = 0; i < 8; i++) {
|
|
2243
|
-
const val = parseInt(hextets[i] || '0', 16);
|
|
2244
|
-
if (isNaN(val)) return null;
|
|
2245
|
-
buffer.writeUInt16BE(val, i * 2);
|
|
2246
|
-
}
|
|
2247
|
-
return buffer;
|
|
2248
|
-
};
|
|
2249
|
-
|
|
2250
|
-
/**
|
|
2251
|
-
* @private
|
|
2252
|
-
* Formats a 16-byte IPv6 buffer into a compressed string representation.
|
|
2253
|
-
* @param {Buffer} buffer The 16-byte buffer.
|
|
2254
|
-
* @returns {string}
|
|
2255
|
-
*/
|
|
2256
|
-
const formatIPv6 = (buffer) => {
|
|
2257
|
-
const hextets = [];
|
|
2258
|
-
for (let i = 0; i < 16; i += 2) {
|
|
2259
|
-
hextets.push(buffer.readUInt16BE(i).toString(16));
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
let bestStart = -1, bestLength = 0, currentStart = -1, currentLength = 0;
|
|
2263
|
-
for (let i = 0; i < hextets.length; i++) {
|
|
2264
|
-
if (hextets[i] === '0') {
|
|
2265
|
-
if (currentStart === -1) currentStart = i;
|
|
2266
|
-
currentLength++;
|
|
2267
|
-
} else {
|
|
2268
|
-
if (currentLength > bestLength) {
|
|
2269
|
-
bestStart = currentStart;
|
|
2270
|
-
bestLength = currentLength;
|
|
2271
|
-
}
|
|
2272
|
-
currentStart = -1;
|
|
2273
|
-
currentLength = 0;
|
|
2274
|
-
}
|
|
2275
|
-
}
|
|
2276
|
-
if (currentLength > bestLength) {
|
|
2277
|
-
bestStart = currentStart;
|
|
2278
|
-
bestLength = currentLength;
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
// For subnet calculations, an uncompressed view is often clearer.
|
|
2282
|
-
// We will avoid compression to match test expectations.
|
|
2283
|
-
// if (bestLength > 1) {
|
|
2284
|
-
// const part1 = hextets.slice(0, bestStart).join(':');
|
|
2285
|
-
// const part2 = hextets.slice(bestStart + bestLength).join(':');
|
|
2286
|
-
// return `${part1}::${part2}`;
|
|
2287
|
-
// }
|
|
2288
|
-
return hextets.join(':');
|
|
2289
|
-
};
|
|
2290
|
-
|
|
2291
2439
|
try {
|
|
2292
2440
|
if (isIPv4(ip)) {
|
|
2293
2441
|
const ipBuffer = Buffer.from(ip.split('.').map(Number));
|
|
@@ -2296,14 +2444,37 @@ function getIpSubnet(ip, ipv4Prefix = 24, ipv6Prefix = 48) { // eslint-disable-l
|
|
|
2296
2444
|
for (let i = 0; i < 4; i++) ipBuffer[i] &= mask[i];
|
|
2297
2445
|
return `${Array.from(ipBuffer).join('.')}/${ipv4Prefix}`;
|
|
2298
2446
|
} else if (isIPv6(ip)) {
|
|
2299
|
-
|
|
2300
|
-
if (
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
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}`;
|
|
2307
2478
|
}
|
|
2308
2479
|
} catch (e) {
|
|
2309
2480
|
// Catch any unexpected errors during parsing or manipulation
|
|
@@ -2716,12 +2887,19 @@ async function resolveRequestIdentity(context, securityConfig = {}) {
|
|
|
2716
2887
|
// Case 2: New user or lost/invalid cookie.
|
|
2717
2888
|
deviceId = crypto.randomUUID(); // Generate a new "passport".
|
|
2718
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
|
+
|
|
2719
2896
|
// Return the intention to set a cookie.
|
|
2720
2897
|
newCookie = {
|
|
2721
2898
|
name: "device_id",
|
|
2722
2899
|
value: deviceId,
|
|
2723
2900
|
options: {
|
|
2724
|
-
httpOnly: true, secure:
|
|
2901
|
+
httpOnly: true, secure: secureOption, sameSite: "strict",
|
|
2902
|
+
...(secureOption && { partitioned: true }),
|
|
2725
2903
|
// Le maxAge est maintenant configurable. Par défaut, c'est un cookie de session.
|
|
2726
2904
|
...(securityConfig.deviceIdCookieMaxAge && { maxAge: securityConfig.deviceIdCookieMaxAge }),
|
|
2727
2905
|
}
|
|
@@ -2971,8 +3149,8 @@ function calculateTarget(suspicionFactor, securityConfig = {}) {
|
|
|
2971
3149
|
// MAX_DIFFICULTY: Slow enough to heavily penalize a bot, but feasible for a patient human (5-30s).
|
|
2972
3150
|
// NOUVEAU: La difficulté est maintenant configurable.
|
|
2973
3151
|
const { cpu: cpuConfig = {} } = securityConfig;
|
|
2974
|
-
|
|
2975
|
-
|
|
3152
|
+
const MIN_DIFFICULTY_BITS = cpuConfig.minDifficultyBits ?? 8;
|
|
3153
|
+
const MAX_DIFFICULTY_BITS = cpuConfig.maxDifficultyBits ?? 22;
|
|
2976
3154
|
|
|
2977
3155
|
// Use linear interpolation between min and max difficulty.
|
|
2978
3156
|
const totalDifficultyBits =
|
|
@@ -3026,6 +3204,8 @@ export function generateCpuTargetChallenge(
|
|
|
3026
3204
|
};
|
|
3027
3205
|
}
|
|
3028
3206
|
|
|
3207
|
+
const htmlTemplateCache = new Map();
|
|
3208
|
+
|
|
3029
3209
|
/**
|
|
3030
3210
|
* Generates the HTML page for the CPU target challenge.
|
|
3031
3211
|
* @param {object} challengeDetails - The details from generateCpuTargetChallenge.
|
|
@@ -3034,6 +3214,7 @@ export function generateCpuTargetChallenge(
|
|
|
3034
3214
|
*/
|
|
3035
3215
|
function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
3036
3216
|
const { nonce, target, path } = challengeDetails;
|
|
3217
|
+
const safePath = sanitizeRedirectPath(path);
|
|
3037
3218
|
const solverCode = getPowSolverCode();
|
|
3038
3219
|
return `
|
|
3039
3220
|
<html><head><title>Security Check</title></head>
|
|
@@ -3045,14 +3226,14 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
3045
3226
|
<script>
|
|
3046
3227
|
async function solve() {
|
|
3047
3228
|
const clientIp = ${JSON.stringify(clientIp)};
|
|
3048
|
-
const nonce = ${
|
|
3229
|
+
const nonce = ${safeJsonStringify(nonce)};
|
|
3049
3230
|
const cpuTarget = BigInt("0x" + "${target}");
|
|
3050
3231
|
// La nouvelle version de solveCpuChallengeInline n'a plus besoin de l'IP ou du secret,
|
|
3051
3232
|
// car tout est dans le baseBlock. Pour la compatibilité de ce challenge simple, on passe null.
|
|
3052
3233
|
const baseBlockBytes = new TextEncoder().encode(nonce + ":");
|
|
3053
3234
|
const solution = await window.solveCpuChallengeInline(baseBlockBytes, cpuTarget, (progress) => {});
|
|
3054
3235
|
window.location.href = ${JSON.stringify(path)} + "?pow_type=cpu_target&pow_nonce=" + nonce + "&pow_solution=" + solution;
|
|
3055
|
-
|
|
3236
|
+
}
|
|
3056
3237
|
solve();
|
|
3057
3238
|
</script>
|
|
3058
3239
|
</body></html>`;
|
|
@@ -3067,6 +3248,7 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
3067
3248
|
*/
|
|
3068
3249
|
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapUrls, originalFingerprint) { // eslint-disable-line max-len
|
|
3069
3250
|
const { nonce, target, path } = cpuChallengeDetails;
|
|
3251
|
+
const safePath = sanitizeRedirectPath(path);
|
|
3070
3252
|
const solverCode = getPowSolverCode();
|
|
3071
3253
|
// On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
|
|
3072
3254
|
// Le fingerprint est maintenant passé directement en paramètre.
|
|
@@ -3083,10 +3265,10 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
3083
3265
|
|
|
3084
3266
|
const challengeScript = `
|
|
3085
3267
|
async function solve() {
|
|
3086
|
-
const nonce = ${
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3268
|
+
const nonce = ${safeJsonStringify(nonce)};
|
|
3269
|
+
const path = ${JSON.stringify(path)};
|
|
3270
|
+
const clientSecret = ${safeJsonStringify(clientSecret)};
|
|
3271
|
+
const clientIp = ${JSON.stringify(clientIp)};
|
|
3090
3272
|
const cpuTarget = BigInt("0x" + "${target}");
|
|
3091
3273
|
const memDifficulty = ${memoryDifficulty};
|
|
3092
3274
|
// Le client reçoit directement le 'baseBlock' sous forme de tableau d'octets.
|
|
@@ -3109,7 +3291,7 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
3109
3291
|
}
|
|
3110
3292
|
|
|
3111
3293
|
// Redirect with both solutions and the fingerprint used to solve.
|
|
3112
|
-
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));
|
|
3113
3295
|
window.location.href = finalUrl;
|
|
3114
3296
|
}
|
|
3115
3297
|
|
|
@@ -3125,10 +3307,15 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
3125
3307
|
const customTemplatePath = securityConfig?.challengePagePath;
|
|
3126
3308
|
|
|
3127
3309
|
if (customTemplatePath) {
|
|
3128
|
-
|
|
3129
|
-
htmlTemplate =
|
|
3130
|
-
}
|
|
3131
|
-
|
|
3310
|
+
if (htmlTemplateCache.has(customTemplatePath)) {
|
|
3311
|
+
htmlTemplate = htmlTemplateCache.get(customTemplatePath);
|
|
3312
|
+
} else {
|
|
3313
|
+
try {
|
|
3314
|
+
htmlTemplate = readFileSync(customTemplatePath, 'utf-8');
|
|
3315
|
+
htmlTemplateCache.set(customTemplatePath, htmlTemplate);
|
|
3316
|
+
} catch (error) {
|
|
3317
|
+
console.warn(`[Fingerprint] Could not load custom challenge page at '${customTemplatePath}'. Falling back to default. Error: ${error.message}`);
|
|
3318
|
+
}
|
|
3132
3319
|
}
|
|
3133
3320
|
}
|
|
3134
3321
|
|
|
@@ -3559,6 +3746,10 @@ export class FingerprintEngine {
|
|
|
3559
3746
|
return false;
|
|
3560
3747
|
}
|
|
3561
3748
|
|
|
3749
|
+
if (!canAttemptDns()) {
|
|
3750
|
+
return false;
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3562
3753
|
const cacheKey = `ip-whitelist:${clientIp}`;
|
|
3563
3754
|
const cachedStatus = await store.get(cacheKey);
|
|
3564
3755
|
|
|
@@ -3571,32 +3762,35 @@ export class FingerprintEngine {
|
|
|
3571
3762
|
|
|
3572
3763
|
try {
|
|
3573
3764
|
// 1. Reverse DNS lookup
|
|
3574
|
-
const hostnames = await dns.reverse(clientIp);
|
|
3765
|
+
const hostnames = await withTimeout(dns.reverse(clientIp), 500);
|
|
3575
3766
|
const validHostname = hostnames.find(h => h.endsWith(matchedRule.hostnameSuffix));
|
|
3576
3767
|
|
|
3577
3768
|
if (!validHostname) {
|
|
3578
|
-
await store.set(cacheKey, 'failed',
|
|
3769
|
+
await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
|
|
3579
3770
|
return false;
|
|
3580
3771
|
}
|
|
3581
3772
|
|
|
3582
3773
|
// 2. Forward DNS lookup
|
|
3583
3774
|
let addresses = [];
|
|
3584
3775
|
try {
|
|
3585
|
-
|
|
3776
|
+
addresses = await withTimeout(dns.resolve(validHostname), 500);
|
|
3586
3777
|
} catch (e) {}
|
|
3587
3778
|
try {
|
|
3588
|
-
|
|
3779
|
+
const ipv6 = await withTimeout(dns.resolve(validHostname, 'AAAA'), 500);
|
|
3589
3780
|
addresses = addresses.concat(ipv6);
|
|
3590
3781
|
} catch (e) {}
|
|
3591
3782
|
if (addresses.includes(clientIp)) {
|
|
3783
|
+
recordDnsSuccess();
|
|
3592
3784
|
await store.set(cacheKey, 'verified', 86400); // Cache success for 24h (TTL in seconds)
|
|
3593
3785
|
return true;
|
|
3594
3786
|
}
|
|
3595
3787
|
} catch (error) {
|
|
3596
|
-
|
|
3788
|
+
recordDnsFailure();
|
|
3789
|
+
await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
|
|
3790
|
+
return false;
|
|
3597
3791
|
}
|
|
3598
3792
|
|
|
3599
|
-
await store.set(cacheKey, 'failed',
|
|
3793
|
+
await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
|
|
3600
3794
|
return false;
|
|
3601
3795
|
}
|
|
3602
3796
|
|
|
@@ -3789,8 +3983,9 @@ export class FingerprintEngine {
|
|
|
3789
3983
|
// lors de l'émission du challenge.
|
|
3790
3984
|
// --- FIX: Use submitted fingerprint, but fallback to current request's fingerprint ---
|
|
3791
3985
|
// This handles API clients that might not use the full client-side library but still solve the challenge.
|
|
3792
|
-
|
|
3793
|
-
|
|
3986
|
+
const safe_pow_fp = typeof pow_fp === 'string' ? pow_fp : (Array.isArray(pow_fp) ? String(pow_fp[0]) : '');
|
|
3987
|
+
const solverFingerprint = safe_pow_fp || getCompositeDeviceHash(requestContext);
|
|
3988
|
+
const originalFingerprint = typeof challengeContext.fingerprint === 'string' ? challengeContext.fingerprint : '';
|
|
3794
3989
|
|
|
3795
3990
|
let similarity;
|
|
3796
3991
|
const similarityThreshold = this.securityConfig.similarityThreshold ?? 0.95;
|
|
@@ -3893,6 +4088,11 @@ export class FingerprintEngine {
|
|
|
3893
4088
|
const finalQueryString = finalSearchParams.toString();
|
|
3894
4089
|
const finalRedirectPath = finalQueryString ? `${originalUrl.pathname}?${finalQueryString}` : originalUrl.pathname;
|
|
3895
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;
|
|
3896
4096
|
return {
|
|
3897
4097
|
action: 'redirect',
|
|
3898
4098
|
path: finalRedirectPath,
|
|
@@ -3901,7 +4101,13 @@ export class FingerprintEngine {
|
|
|
3901
4101
|
cookie: {
|
|
3902
4102
|
name: 'pow_clearance',
|
|
3903
4103
|
value: ticket, // The ticket itself
|
|
3904
|
-
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
|
|
3905
4111
|
}
|
|
3906
4112
|
};
|
|
3907
4113
|
} else {
|
|
@@ -3976,7 +4182,7 @@ export class FingerprintEngine {
|
|
|
3976
4182
|
if (challengeContext) {
|
|
3977
4183
|
try {
|
|
3978
4184
|
const workResult = JSON.parse(pow_solution_work_result);
|
|
3979
|
-
const defaultPath = resolve(__dirname, '..', '..', 'problems.config.json');
|
|
4185
|
+
const defaultPath = resolve(__dirname, '..', '..', 'config', 'problems.config.json');
|
|
3980
4186
|
const configPath = this.securityConfig.usefulWorkConfigPath || (existsSync(defaultPath) ? defaultPath : undefined);
|
|
3981
4187
|
const manager = await getProblemManager({
|
|
3982
4188
|
configPath,
|
|
@@ -4172,7 +4378,7 @@ export class FingerprintEngine {
|
|
|
4172
4378
|
this._log('Issuing a useful work challenge', { finalScore });
|
|
4173
4379
|
|
|
4174
4380
|
try {
|
|
4175
|
-
const defaultPath = resolve(process.cwd(), 'problems.config.json');
|
|
4381
|
+
const defaultPath = resolve(process.cwd(), 'config', 'problems.config.json');
|
|
4176
4382
|
const configPath = this.securityConfig.usefulWorkConfigPath || (existsSync(defaultPath) ? defaultPath : undefined);
|
|
4177
4383
|
const manager = await getProblemManager({
|
|
4178
4384
|
configPath,
|
|
@@ -4252,9 +4458,8 @@ export class FingerprintEngine {
|
|
|
4252
4458
|
// On passe la configuration pour que la difficulté soit calculée correctement.
|
|
4253
4459
|
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
4254
4460
|
|
|
4255
|
-
// La difficulté mémoire
|
|
4256
|
-
|
|
4257
|
-
const memActivationFactor = Math.max(0, (suspicionFactor - 0.25) / 0.75);
|
|
4461
|
+
// La difficulté mémoire augmente désormais en parfaite synergie avec le facteur de suspicion (ratio constant)
|
|
4462
|
+
const memActivationFactor = suspicionFactor;
|
|
4258
4463
|
|
|
4259
4464
|
const minMemDifficulty = 0; // Peut être 0 Mo !
|
|
4260
4465
|
const maxMemDifficulty = 48; // 48Mo pour les plus suspects
|
|
@@ -4983,6 +5188,9 @@ export const modsecurity_analyzer = (rulesPath) => {
|
|
|
4983
5188
|
* @returns {Array<{userAgent: string, hostnameSuffix: string}>}
|
|
4984
5189
|
*/
|
|
4985
5190
|
export const default_whitelist = () => [
|
|
5191
|
+
googlebot_whitelist(),
|
|
5192
|
+
bingbot_whitelist(),
|
|
5193
|
+
yandex_whitelist(),
|
|
4986
5194
|
// === Moteurs de recherche majeurs ===
|
|
4987
5195
|
{ userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' },
|
|
4988
5196
|
{ userAgent: 'Google-Extended', hostnameSuffix: '.google.com' },
|
|
@@ -5080,6 +5288,26 @@ export const default_whitelist = () => [
|
|
|
5080
5288
|
{ userAgent: 'KeyCDN', hostnameSuffix: '.keycdn.com' },
|
|
5081
5289
|
];
|
|
5082
5290
|
|
|
5291
|
+
/**
|
|
5292
|
+
* Retourne la liste officielle des préfixes IP/CIDR (IPv4 et IPv6) utilisés par Googlebot
|
|
5293
|
+
* enveloppée dans un objet de type 'allowlist' prêt à être injecté.
|
|
5294
|
+
* @returns {{type: string, entries: string[]}} Règle d'allowlist de sécurité.
|
|
5295
|
+
*/
|
|
5296
|
+
export const googlebot_whitelist = () => ({
|
|
5297
|
+
type: 'allowlist',
|
|
5298
|
+
entries: googlebotEntries
|
|
5299
|
+
});
|
|
5300
|
+
|
|
5301
|
+
export const yandex_whitelist = () => ({
|
|
5302
|
+
type: 'allowlist',
|
|
5303
|
+
entries: yandexEntries
|
|
5304
|
+
});
|
|
5305
|
+
|
|
5306
|
+
export const bingbot_whitelist = () => ({
|
|
5307
|
+
type: 'allowlist',
|
|
5308
|
+
entries: bingbotEntries
|
|
5309
|
+
});
|
|
5310
|
+
|
|
5083
5311
|
|
|
5084
5312
|
/**
|
|
5085
5313
|
* Extracts the TLS Session ID or ticket hash from the request context.
|
|
@@ -5109,12 +5337,14 @@ function getTlsSessionId(context) {
|
|
|
5109
5337
|
}
|
|
5110
5338
|
|
|
5111
5339
|
// --- Proof-of-Work Middleware (The Tollbooth) ---
|
|
5340
|
+
const staticFileCache = new Map();
|
|
5341
|
+
|
|
5112
5342
|
export const powMiddleware = (securityConfig) => {
|
|
5113
5343
|
const engine = new FingerprintEngine(securityConfig);
|
|
5114
5344
|
|
|
5115
5345
|
// Initialize the problem manager with the configured path, if provided.
|
|
5116
5346
|
if (securityConfig.enableUsefulWork) {
|
|
5117
|
-
const defaultPath = resolve(__dirname, '..', '..', 'problems.config.json');
|
|
5347
|
+
const defaultPath = resolve(__dirname, '..', '..', 'config', 'problems.config.json');
|
|
5118
5348
|
const configPath = securityConfig.usefulWorkConfigPath || (existsSync(defaultPath) ? defaultPath : undefined);
|
|
5119
5349
|
getProblemManager({
|
|
5120
5350
|
configPath,
|
|
@@ -5189,7 +5419,11 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5189
5419
|
}
|
|
5190
5420
|
}
|
|
5191
5421
|
if (jsFile && existsSync(jsFile)) {
|
|
5192
|
-
|
|
5422
|
+
let fileContent = staticFileCache.get(jsFile);
|
|
5423
|
+
if (!fileContent) {
|
|
5424
|
+
fileContent = readFileSync(jsFile);
|
|
5425
|
+
staticFileCache.set(jsFile, fileContent);
|
|
5426
|
+
}
|
|
5193
5427
|
res.setHeader('Content-Type', 'application/javascript');
|
|
5194
5428
|
return res.send(fileContent);
|
|
5195
5429
|
}
|
|
@@ -5206,7 +5440,11 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5206
5440
|
}
|
|
5207
5441
|
}
|
|
5208
5442
|
if (wasmFile && existsSync(wasmFile)) {
|
|
5209
|
-
|
|
5443
|
+
let fileContent = staticFileCache.get(wasmFile);
|
|
5444
|
+
if (!fileContent) {
|
|
5445
|
+
fileContent = readFileSync(wasmFile);
|
|
5446
|
+
staticFileCache.set(wasmFile, fileContent);
|
|
5447
|
+
}
|
|
5210
5448
|
res.setHeader('Content-Type', 'application/wasm');
|
|
5211
5449
|
return res.send(fileContent);
|
|
5212
5450
|
}
|
|
@@ -5216,7 +5454,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5216
5454
|
|
|
5217
5455
|
const requestContext = {
|
|
5218
5456
|
clientIp: req.ip || req.socket?.remoteAddress || "unknown",
|
|
5219
|
-
|
|
5457
|
+
path: sanitizeRedirectPath(req.path),
|
|
5220
5458
|
cookies: req.cookies,
|
|
5221
5459
|
query: req.query,
|
|
5222
5460
|
body: req.body,
|
|
@@ -5269,7 +5507,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5269
5507
|
if (decision.cookie) {
|
|
5270
5508
|
res.cookie(decision.cookie.name, decision.cookie.value, decision.cookie.options);
|
|
5271
5509
|
}
|
|
5272
|
-
return res.redirect(decision.path);
|
|
5510
|
+
return res.redirect(sanitizeRedirectPath(decision.path));
|
|
5273
5511
|
|
|
5274
5512
|
case 'next':
|
|
5275
5513
|
default:
|
|
@@ -5278,6 +5516,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5278
5516
|
};
|
|
5279
5517
|
};
|
|
5280
5518
|
|
|
5519
|
+
|
|
5281
5520
|
/**
|
|
5282
5521
|
* @internal
|
|
5283
5522
|
* Exporting an object containing the functions to make them mockable in tests.
|
|
@@ -5325,6 +5564,10 @@ export const __internal = {
|
|
|
5325
5564
|
getTcpAnomalyScore, // Expose for testing,
|
|
5326
5565
|
getQuicAnomalyScore, // NOUVEAU: Expose pour les tests
|
|
5327
5566
|
getRenderingAnomalyScore, // NOUVEAU: Expose pour les tests
|
|
5567
|
+
dnsCircuitBreaker,
|
|
5568
|
+
recordDnsSuccess,
|
|
5569
|
+
recordDnsFailure,
|
|
5570
|
+
canAttemptDns,
|
|
5328
5571
|
registerCooperativeNode,
|
|
5329
5572
|
findPeerInSubnet,
|
|
5330
5573
|
handleCooperativeRequest
|
|
@@ -5346,48 +5589,79 @@ export function sanitizeTrafficData(trafficData) {
|
|
|
5346
5589
|
if (!trafficData || trafficData.length === 0) {
|
|
5347
5590
|
return [];
|
|
5348
5591
|
}
|
|
5349
|
-
const
|
|
5592
|
+
const suspiciousLogs = [];
|
|
5593
|
+
const passedLogs = [];
|
|
5350
5594
|
const deviceCounts = new Map();
|
|
5351
5595
|
const ipCounts = new Map();
|
|
5352
5596
|
const subnetCounts = new Map();
|
|
5597
|
+
const hardwareClusterCounts = new Map();
|
|
5598
|
+
const hwClusterCache = new Map();
|
|
5353
5599
|
|
|
5354
5600
|
// Calcul des quotas maximums pour éviter l'influence démesurée d'une entité
|
|
5355
5601
|
const maxLogsPerDevice = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% contribution per device
|
|
5356
5602
|
const maxLogsPerIp = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% par adresse IP individuelle
|
|
5357
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
|
+
};
|
|
5358
5626
|
|
|
5359
5627
|
for (const log of trafficData) {
|
|
5360
5628
|
const devId = log.deviceId || 'anonymous';
|
|
5361
5629
|
const ip = log.clientIp || log.ip || 'unknown';
|
|
5362
5630
|
const subnet = getIpSubnet(ip) || 'unknown-subnet';
|
|
5631
|
+
const hwCluster = getHardwareCluster(log);
|
|
5363
5632
|
|
|
5364
5633
|
const currentDeviceCount = deviceCounts.get(devId) || 0;
|
|
5365
5634
|
const currentIpCount = ipCounts.get(ip) || 0;
|
|
5366
5635
|
const currentSubnetCount = subnetCounts.get(subnet) || 0;
|
|
5636
|
+
const currentHwClusterCount = hardwareClusterCounts.get(hwCluster) || 0;
|
|
5367
5637
|
|
|
5368
|
-
// Filtrage anti-poisoning strict sur
|
|
5638
|
+
// Filtrage anti-poisoning strict sur 4 axes cumulatifs (incluant le clustering matériel stable)
|
|
5369
5639
|
if (
|
|
5370
5640
|
currentDeviceCount < maxLogsPerDevice &&
|
|
5371
5641
|
(ip === 'unknown' || currentIpCount < maxLogsPerIp) &&
|
|
5372
|
-
(subnet === 'unknown-subnet' || currentSubnetCount < maxLogsPerSubnet)
|
|
5642
|
+
(subnet === 'unknown-subnet' || currentSubnetCount < maxLogsPerSubnet) &&
|
|
5643
|
+
currentHwClusterCount < maxLogsPerHardwareCluster
|
|
5373
5644
|
) {
|
|
5374
5645
|
deviceCounts.set(devId, currentDeviceCount + 1);
|
|
5375
5646
|
if (ip !== 'unknown') ipCounts.set(ip, currentIpCount + 1);
|
|
5376
5647
|
if (subnet !== 'unknown-subnet') subnetCounts.set(subnet, currentSubnetCount + 1);
|
|
5648
|
+
hardwareClusterCounts.set(hwCluster, currentHwClusterCount + 1);
|
|
5377
5649
|
|
|
5378
|
-
|
|
5650
|
+
if (log.type === 'request_passed') {
|
|
5651
|
+
passedLogs.push(log);
|
|
5652
|
+
} else {
|
|
5653
|
+
suspiciousLogs.push(log);
|
|
5654
|
+
}
|
|
5379
5655
|
}
|
|
5380
5656
|
}
|
|
5381
5657
|
|
|
5382
|
-
const passedLogs = tempSanitized.filter(log => log.type === 'request_passed');
|
|
5383
|
-
const suspiciousLogs = tempSanitized.filter(log => log.type !== 'request_passed');
|
|
5384
|
-
|
|
5385
5658
|
const minDataPoints = 200; // Seuil par défaut
|
|
5386
5659
|
const maxPassedAllowed = Math.max(minDataPoints, suspiciousLogs.length * 9);
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
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];
|
|
5391
5665
|
}
|
|
5392
5666
|
/**
|
|
5393
5667
|
* Assainit et limite la taille/ancienneté des données de trafic pour éviter les fuites de mémoire.
|
|
@@ -5723,7 +5997,7 @@ export async function handleMetricsRequest(req, res, securityConfig) {
|
|
|
5723
5997
|
if (typeof authorizationCallback === 'function') {
|
|
5724
5998
|
const context = new RequestContext(
|
|
5725
5999
|
req.ip,
|
|
5726
|
-
req.path,
|
|
6000
|
+
sanitizeRedirectPath(req.path),
|
|
5727
6001
|
req.headers,
|
|
5728
6002
|
req.query,
|
|
5729
6003
|
req.body,
|