@anonympins/fingerprint 0.1.1 → 0.1.3
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/README.md +44 -0
- package/fingerprint.client.js +4 -4
- package/fingerprint.js +134 -62
- package/package.json +76 -76
- package/pow.solver.js +223 -213
package/README.md
CHANGED
|
@@ -97,6 +97,7 @@ const securityConfig = {
|
|
|
97
97
|
ticketMaxAge: 3600000, // 1 hour. Duration for which a solved challenge ticket is valid.
|
|
98
98
|
challengeTtl: 300000, // 5 minutes. Time during which a challenge nonce is valid.
|
|
99
99
|
deviceIdCookieMaxAge: undefined, // By default, it's a session cookie. Set a value in ms for a persistent cookie.
|
|
100
|
+
challengePagePath: './path/to/your/custom-challenge-page.html', // (Optional) Path to a custom HTML template for the challenge page.
|
|
100
101
|
verbose: true, // set to true to log for fingerprint detection output
|
|
101
102
|
patterns: { // (Optional) Initial values for request pattern detection, optimized by auto-tuner if enabled.
|
|
102
103
|
velocityThreshold: 800, // ms between requests to be considered "fast"
|
|
@@ -199,7 +200,50 @@ app.use((req, res, next) => {
|
|
|
199
200
|
|
|
200
201
|
app.listen(3000, () => console.log('Server started on port 3000'));
|
|
201
202
|
```
|
|
203
|
+
### Customizing the Challenge Page
|
|
202
204
|
|
|
205
|
+
You can provide your own HTML template for the Proof-of-Work challenge page to maintain a consistent user experience with your brand.
|
|
206
|
+
|
|
207
|
+
1. **Configuration**: In your `securityConfig`, specify the path to your template file using the `challengePagePath` option.
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
const securityConfig = {
|
|
211
|
+
// ... other options
|
|
212
|
+
challengePagePath: './path/to/your/custom-challenge-page.html',
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
2. **Template Placeholders**: Your HTML file **must** contain the following placeholders. The system will replace them with the dynamic JavaScript code required to run the challenge.
|
|
217
|
+
|
|
218
|
+
* `<!-- FINGERPRINT_SOLVER_SCRIPT -->`: This will be replaced by the script that contains the logic for solving the CPU and memory challenges.
|
|
219
|
+
* `<!-- FINGERPRINT_CHALLENGE_SCRIPT -->`: This will be replaced by the script that initiates the challenge with the specific parameters for the current request (nonce, difficulty, etc.).
|
|
220
|
+
* `<!-- FINGERPRINT_TRAPS -->`: This will be replaced by hidden "honeypot" links designed to trap simple bots. This placeholder is crucial for an effective defense.
|
|
221
|
+
|
|
222
|
+
#### Example Custom HTML Template
|
|
223
|
+
|
|
224
|
+
Here is a basic example of what your `custom-challenge-page.html` could look like:
|
|
225
|
+
|
|
226
|
+
```html
|
|
227
|
+
<!DOCTYPE html>
|
|
228
|
+
<html lang="en">
|
|
229
|
+
<head>
|
|
230
|
+
<meta charset="UTF-8">
|
|
231
|
+
<title>Security Verification</title>
|
|
232
|
+
<style>
|
|
233
|
+
body { font-family: sans-serif; text-align: center; padding-top: 50px; }
|
|
234
|
+
h1 { color: #333; }
|
|
235
|
+
</style>
|
|
236
|
+
</head>
|
|
237
|
+
<body>
|
|
238
|
+
<h1>Please wait while we verify your connection...</h1>
|
|
239
|
+
<div id="loader" style="margin:20px;">⚙️ Initializing verification...</div>
|
|
240
|
+
|
|
241
|
+
<script><!-- FINGERPRINT_SOLVER_SCRIPT --></script>
|
|
242
|
+
<script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script>
|
|
243
|
+
<!-- FINGERPRINT_TRAPS -->
|
|
244
|
+
</body>
|
|
245
|
+
</html>
|
|
246
|
+
```
|
|
203
247
|
## Public API
|
|
204
248
|
|
|
205
249
|
In addition to the main middleware, several functions are exported to allow for more advanced integrations.
|
package/fingerprint.client.js
CHANGED
|
@@ -238,9 +238,9 @@ const ClientLibrary = {
|
|
|
238
238
|
|
|
239
239
|
_isFetchPatched: false,
|
|
240
240
|
_interceptorChain: [],
|
|
241
|
-
// On
|
|
242
|
-
//
|
|
243
|
-
_originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) :
|
|
241
|
+
// On stocke la fonction fetch originale et on la lie à son contexte (window)
|
|
242
|
+
// pour éviter les erreurs "Illegal invocation" si une autre lib la modifie.
|
|
243
|
+
_originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) : null,
|
|
244
244
|
|
|
245
245
|
/**
|
|
246
246
|
* Adds an interceptor function to the `fetch` chain.
|
|
@@ -256,7 +256,7 @@ const ClientLibrary = {
|
|
|
256
256
|
},
|
|
257
257
|
|
|
258
258
|
patchGlobalFetch() {
|
|
259
|
-
if (this._isFetchPatched ||
|
|
259
|
+
if (this._isFetchPatched || !this._originalFetch) return;
|
|
260
260
|
|
|
261
261
|
this._isFetchPatched = true;
|
|
262
262
|
window.fetch = (resource, options) => {
|
package/fingerprint.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// C:/Dev/games.primals.net/src/utils/fingerprint.js
|
|
2
1
|
import crypto from "node:crypto";
|
|
3
2
|
import { BlockList } from "node:net";
|
|
4
3
|
import dns from "node:dns/promises";
|
|
@@ -30,17 +29,19 @@ const getPowSolverCode = () => {
|
|
|
30
29
|
try {
|
|
31
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
32
31
|
const __dirname = dirname(__filename);
|
|
33
|
-
const solverPath = join(__dirname, 'pow.solver.js');
|
|
32
|
+
const solverPath = join(__dirname, 'pow.solver.inline.js'); // Use the inline version
|
|
34
33
|
return readFileSync(solverPath, 'utf-8');
|
|
35
34
|
} catch (error) {
|
|
36
35
|
console.warn('Could not load pow.solver.js for inlining, using fallback inline code');
|
|
37
36
|
// Fallback inline code if file cannot be loaded
|
|
38
37
|
return `(function(global){
|
|
39
|
-
async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){
|
|
38
|
+
async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){ const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
|
|
40
39
|
const cpuTarget = BigInt(target);
|
|
41
40
|
let cpuSolution = 0;
|
|
41
|
+
const ipPart = clientIp || '';
|
|
42
42
|
while(true){
|
|
43
|
-
|
|
43
|
+
// When a clientSecret is used, the IP is omitted from the hash to make it independent of the network.
|
|
44
|
+
const msg = clientSecret ? \`\${nonce}:\${cpuSolution}:\${clientSecret}\` : \`\${ipPart}:\${nonce}:\${cpuSolution}\`;
|
|
44
45
|
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
|
|
45
46
|
const hashHex = Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join('');
|
|
46
47
|
if(BigInt('0x'+hashHex) < cpuTarget) break;
|
|
@@ -95,9 +96,39 @@ const getPowSolverCode = () => {
|
|
|
95
96
|
const solutionDistance=evaluatePathDistance(cities,solutionPath);
|
|
96
97
|
return{path:solutionPath,distance:solutionDistance};
|
|
97
98
|
}
|
|
99
|
+
async function solveChallenge(challenge) {
|
|
100
|
+
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
|
|
101
|
+
const solutions = {};
|
|
102
|
+
|
|
103
|
+
switch (type) {
|
|
104
|
+
case 'cpu_target':
|
|
105
|
+
solutions.cpu = await solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret);
|
|
106
|
+
break;
|
|
107
|
+
case 'cpu_mem':
|
|
108
|
+
case 'cpu_mem_inline':
|
|
109
|
+
const memSeed = nonce + ":" + clientSecret;
|
|
110
|
+
const [cpuSol, memSol] = await Promise.all([
|
|
111
|
+
solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret),
|
|
112
|
+
solveMemory(memSeed, memDifficulty)
|
|
113
|
+
]);
|
|
114
|
+
solutions.cpu = cpuSol;
|
|
115
|
+
solutions.mem = memSol;
|
|
116
|
+
break;
|
|
117
|
+
case 'tsp':
|
|
118
|
+
const tspResult = await solveTsp(cities, targetMaxDistance);
|
|
119
|
+
solutions.tsp = tspResult.path;
|
|
120
|
+
solutions.distance = tspResult.distance;
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
throw new Error(\`Unknown challenge type: \${type}\`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return solutions;
|
|
127
|
+
}
|
|
98
128
|
global.solveCpuChallengeInline=solveCpuTargetInline;
|
|
99
129
|
global.solveMemoryChallenge=solveMemory;
|
|
100
130
|
global.solveTspChallenge=solveTsp;
|
|
131
|
+
global.solveChallenge=solveChallenge;
|
|
101
132
|
})(typeof window!=='undefined'?window:global);`;
|
|
102
133
|
}
|
|
103
134
|
};
|
|
@@ -1224,48 +1255,61 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
1224
1255
|
* @param {string} clientIp - The client's IP address.
|
|
1225
1256
|
* @returns {string} HTML content.
|
|
1226
1257
|
*/
|
|
1227
|
-
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret) {
|
|
1258
|
+
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml) {
|
|
1228
1259
|
const { nonce, target, path } = cpuChallengeDetails;
|
|
1229
1260
|
const solverCode = getPowSolverCode();
|
|
1230
|
-
return `
|
|
1231
|
-
<html><head><title>Advanced Security Check</title></head>
|
|
1232
|
-
<body style="font-family:sans-serif; text-align:center; padding-top:50px;">
|
|
1233
|
-
<h1>Enhanced Verification... (Level 2)</h1>
|
|
1234
|
-
<p>Your activity requires an additional security check. This may take a few moments.</p>
|
|
1235
|
-
<div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div>
|
|
1236
|
-
<script>${solverCode}</script>
|
|
1237
|
-
<script>
|
|
1238
|
-
async function solve() {
|
|
1239
|
-
const nonce = "${nonce}";
|
|
1240
|
-
const path = "${path}";
|
|
1241
|
-
const clientSecret = "${clientSecret}";
|
|
1242
|
-
const clientIp = "${clientIp}";
|
|
1243
|
-
const cpuTarget = BigInt("0x${target}");
|
|
1244
|
-
const memDifficulty = ${memoryDifficulty};
|
|
1245
1261
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1262
|
+
const challengeScript = `
|
|
1263
|
+
async function solve() {
|
|
1264
|
+
const nonce = "${nonce}";
|
|
1265
|
+
const path = "${path}";
|
|
1266
|
+
const clientSecret = "${clientSecret}";
|
|
1267
|
+
const clientIp = "${clientIp}";
|
|
1268
|
+
const cpuTarget = BigInt("0x${target}");
|
|
1269
|
+
const memDifficulty = ${memoryDifficulty};
|
|
1270
|
+
|
|
1271
|
+
// --- CPU Challenge ---
|
|
1272
|
+
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
|
|
1273
|
+
const cpuSolution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, clientSecret, (progress) => {
|
|
1274
|
+
// Optional progress callback
|
|
1275
|
+
});
|
|
1251
1276
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1277
|
+
// --- Memory Challenge ---
|
|
1278
|
+
document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
|
|
1279
|
+
await new Promise(r => setTimeout(r, 10)); // Yield to update UI
|
|
1255
1280
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1281
|
+
let memSolution = 0;
|
|
1282
|
+
try {
|
|
1283
|
+
const memSeed = nonce + ":" + clientSecret;
|
|
1284
|
+
memSolution = await window.solveMemoryChallenge(memSeed, memDifficulty);
|
|
1285
|
+
} catch(e) {
|
|
1286
|
+
document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
1290
|
+
}
|
|
1291
|
+
solve();
|
|
1292
|
+
`;
|
|
1293
|
+
|
|
1294
|
+
let htmlTemplate;
|
|
1295
|
+
const customTemplatePath = securityConfig?.challengePagePath;
|
|
1296
|
+
|
|
1297
|
+
if (customTemplatePath) {
|
|
1298
|
+
try {
|
|
1299
|
+
htmlTemplate = readFileSync(customTemplatePath, 'utf-8');
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
console.warn(`[Fingerprint] Could not load custom challenge page at '${customTemplatePath}'. Falling back to default. Error: ${error.message}`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (!htmlTemplate) {
|
|
1306
|
+
htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script><!-- FINGERPRINT_TRAPS --></body></html>`;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return htmlTemplate
|
|
1310
|
+
.replace('<!-- FINGERPRINT_SOLVER_SCRIPT -->', solverCode)
|
|
1311
|
+
.replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript)
|
|
1312
|
+
.replace('<!-- FINGERPRINT_TRAPS -->', trapContainerHtml);
|
|
1269
1313
|
}
|
|
1270
1314
|
|
|
1271
1315
|
/**
|
|
@@ -1276,20 +1320,19 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
1276
1320
|
ticketMaxAge, // NOUVEAU: Durée de validité du ticket configurable
|
|
1277
1321
|
nonce,
|
|
1278
1322
|
solution,
|
|
1279
|
-
suspicionFactor,
|
|
1280
1323
|
clientSecret, // Le secret est maintenant requis
|
|
1324
|
+
target, // La cible est maintenant passée directement
|
|
1281
1325
|
) {
|
|
1282
|
-
const target = calculateTarget(suspicionFactor);
|
|
1283
1326
|
const message = clientSecret
|
|
1284
|
-
? `${
|
|
1285
|
-
: `${clientIp}:${nonce}:${solution}`;
|
|
1327
|
+
? `${nonce}:${solution}:${clientSecret}` // FIX: Ne pas inclure l'IP si un secret client est utilisé
|
|
1328
|
+
: `${clientIp}:${nonce}:${solution}`; // L'IP est utilisée uniquement pour les challenges sans secret (plus anciens/simples)
|
|
1286
1329
|
const hash = crypto
|
|
1287
1330
|
.createHash("sha256")
|
|
1288
1331
|
.update(message)
|
|
1289
1332
|
.digest("hex");
|
|
1290
1333
|
const hashAsInt = BigInt("0x" + hash);
|
|
1291
1334
|
|
|
1292
|
-
if (hashAsInt < target) {
|
|
1335
|
+
if (hashAsInt < BigInt("0x" + target)) {
|
|
1293
1336
|
// The comparison is direct with native BigInts
|
|
1294
1337
|
// The proof is valid, generate the ticket
|
|
1295
1338
|
const expiry = Date.now() + (ticketMaxAge || 3600000); // Utilise la durée passée ou un fallback.
|
|
@@ -1304,7 +1347,7 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
1304
1347
|
}
|
|
1305
1348
|
|
|
1306
1349
|
const staticExtensions = new RegExp(
|
|
1307
|
-
"\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map|json|manifest)$",
|
|
1350
|
+
"\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map|json|manifest|webmanifest)$",
|
|
1308
1351
|
"i",
|
|
1309
1352
|
);
|
|
1310
1353
|
const isStaticResource = (path) => staticExtensions.test(path);
|
|
@@ -1381,22 +1424,28 @@ function determineOptimalTicketTtl(suspicionScore) {
|
|
|
1381
1424
|
* @private
|
|
1382
1425
|
*/
|
|
1383
1426
|
function isMalicious(str) {
|
|
1427
|
+
if (typeof str !== 'string') return false;
|
|
1384
1428
|
// Regex pour les injections SQL et NoSQL de base
|
|
1385
1429
|
// Ajout de la détection des injections basées sur le temps (SLEEP, BENCHMARK, WAITFOR) et d'autres commandes dangereuses.
|
|
1386
|
-
const injectionRegex = /(\$ne|' OR '1'='1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|SLEEP\(|BENCHMARK\(|WAITFOR DELAY)/i;
|
|
1430
|
+
const injectionRegex = /(\$ne|' *OR *'1'='1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|SLEEP\(|BENCHMARK\(|WAITFOR DELAY)/i;
|
|
1387
1431
|
// Regex pour les injections plus avancées
|
|
1388
1432
|
const log4ShellRegex = /\$\{jndi:(ldap|rmi|dns):/i;
|
|
1389
1433
|
const sstiRegex = /\{\{.*\}\}|\{%.*%\}/; // Détecte les syntaxes de type Jinja2, Twig, etc.
|
|
1390
1434
|
const xxeRegex = /<!ENTITY\s+.*SYSTEM/i;
|
|
1391
1435
|
const pathTraversalRegex = /(\.\.\/|\.\.\\)/;
|
|
1392
|
-
//
|
|
1393
|
-
//
|
|
1394
|
-
|
|
1436
|
+
// Regex pour les injections de commandes.
|
|
1437
|
+
// Elle détecte deux cas :
|
|
1438
|
+
// 1. L'utilisation de backticks `` pour l'exécution de commandes.
|
|
1439
|
+
// 2. Des commandes dangereuses (comme rm, whoami) précédées par un séparateur de commande (;, &&, ||, |)
|
|
1440
|
+
// pour éviter les faux positifs sur des phrases comme "A normal command like ls -la".
|
|
1441
|
+
const commandInjectionRegex = /`.*`|[\n;&|]\s*(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i;
|
|
1395
1442
|
|
|
1396
1443
|
return injectionRegex.test(str) || log4ShellRegex.test(str) || sstiRegex.test(str) || xxeRegex.test(str) || pathTraversalRegex.test(str) || commandInjectionRegex.test(str);
|
|
1397
1444
|
}
|
|
1398
1445
|
|
|
1399
1446
|
// --- Middleware Proof-of-Work (Le péage) ---
|
|
1447
|
+
export { isMalicious };
|
|
1448
|
+
|
|
1400
1449
|
export class FingerprintEngine {
|
|
1401
1450
|
constructor(securityConfig) {
|
|
1402
1451
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
@@ -1580,18 +1629,20 @@ export class FingerprintEngine {
|
|
|
1580
1629
|
let isValid = false;
|
|
1581
1630
|
const challengeContext = await store.get(`secret:${pow_nonce}`);
|
|
1582
1631
|
let ticket = null;
|
|
1632
|
+
// Déclarer optimalTtl ici avec une valeur par défaut
|
|
1633
|
+
let optimalTtl = this.securityConfig.ticketMaxAge || 3600000;
|
|
1583
1634
|
|
|
1584
1635
|
if (challengeContext) {
|
|
1585
|
-
|
|
1636
|
+
optimalTtl = determineOptimalTicketTtl(preliminaryScore);
|
|
1586
1637
|
this._log('Challenge context found, verifying solution', { optimalTtl });
|
|
1587
1638
|
|
|
1588
1639
|
if (pow_type === "cpu_target") {
|
|
1589
1640
|
// On passe la durée de vie du ticket configurée
|
|
1590
|
-
ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, optimalTtl, pow_nonce, pow_solution,
|
|
1641
|
+
ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, optimalTtl, pow_nonce, pow_solution, challengeContext.clientSecret, challengeContext.cpuTarget);
|
|
1591
1642
|
isValid = ticket !== null;
|
|
1592
1643
|
this._log('CPU target challenge verification', { isValid });
|
|
1593
1644
|
} else if (pow_type === "cpu_mem") {
|
|
1594
|
-
const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, optimalTtl, pow_nonce, pow_solution_cpu,
|
|
1645
|
+
const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, optimalTtl, pow_nonce, pow_solution_cpu, challengeContext.clientSecret, challengeContext.cpuTarget);
|
|
1595
1646
|
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret);
|
|
1596
1647
|
isValid = cpuTicket !== null && isMemValid;
|
|
1597
1648
|
if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
|
|
@@ -1605,18 +1656,40 @@ export class FingerprintEngine {
|
|
|
1605
1656
|
this._log('Challenge context not found or expired', { pow_nonce });
|
|
1606
1657
|
}
|
|
1607
1658
|
|
|
1659
|
+
console.log({isValid})
|
|
1608
1660
|
if (isValid) {
|
|
1609
1661
|
// La solution est valide. On supprime le secret et on redirige.
|
|
1610
1662
|
await store.delete(`secret:${pow_nonce}`);
|
|
1611
|
-
this._log('Challenge solution valid - issuing ticket', { ticketMaxAge:
|
|
1663
|
+
this._log('Challenge solution valid - issuing ticket', { ticketMaxAge: optimalTtl });
|
|
1612
1664
|
|
|
1613
1665
|
if (logger) {
|
|
1614
1666
|
logger({ type: 'challenge_solved', deviceId: cookies?.device_id, score: preliminaryScore, challengeType: pow_type, timestamp: Date.now() });
|
|
1615
1667
|
}
|
|
1616
1668
|
|
|
1669
|
+
// NOUVELLE LOGIQUE DE REDIRECTION (plus robuste)
|
|
1670
|
+
// 1. On part du chemin original stocké, qui peut contenir des query params.
|
|
1671
|
+
const originalUrl = new URL(challengeContext?.originalPath || path, `http://${requestContext.headers.host || 'localhost'}`);
|
|
1672
|
+
|
|
1673
|
+
console.log({originalUrl})
|
|
1674
|
+
// 2. On crée un nouvel objet de paramètres à partir de la requête entrante (qui contient les solutions ET les params originaux).
|
|
1675
|
+
const finalSearchParams = new URLSearchParams(requestContext.query);
|
|
1676
|
+
|
|
1677
|
+
// 3. On supprime uniquement les paramètres liés au challenge.
|
|
1678
|
+
finalSearchParams.delete('pow_type');
|
|
1679
|
+
finalSearchParams.delete('pow_nonce');
|
|
1680
|
+
finalSearchParams.delete('pow_solution');
|
|
1681
|
+
finalSearchParams.delete('pow_solution_cpu');
|
|
1682
|
+
finalSearchParams.delete('pow_solution_mem');
|
|
1683
|
+
|
|
1684
|
+
// 4. On reconstruit le chemin final.
|
|
1685
|
+
const finalQueryString = finalSearchParams.toString();
|
|
1686
|
+
const finalRedirectPath = finalQueryString ? `${originalUrl.pathname}?${finalQueryString}` : originalUrl.pathname;
|
|
1687
|
+
this._log('Redirecting to clean path', { finalRedirectPath });
|
|
1688
|
+
|
|
1689
|
+
console.log({finalRedirectPath})
|
|
1617
1690
|
return {
|
|
1618
1691
|
action: 'redirect',
|
|
1619
|
-
path:
|
|
1692
|
+
path: finalRedirectPath,
|
|
1620
1693
|
score: 0, // Le score n'est pas pertinent ici, on a passé le test.
|
|
1621
1694
|
vector: { challenge_solved: 100 },
|
|
1622
1695
|
cookie: {
|
|
@@ -1625,7 +1698,7 @@ export class FingerprintEngine {
|
|
|
1625
1698
|
options: {
|
|
1626
1699
|
httpOnly: true,
|
|
1627
1700
|
secure: this.isProduction, // Le maxAge est déjà inclus dans le ticket, mais on le met aussi sur le cookie
|
|
1628
|
-
maxAge:
|
|
1701
|
+
maxAge: optimalTtl,
|
|
1629
1702
|
}
|
|
1630
1703
|
}
|
|
1631
1704
|
};
|
|
@@ -1791,7 +1864,8 @@ export class FingerprintEngine {
|
|
|
1791
1864
|
await store.set(`secret:${nonce}`, {
|
|
1792
1865
|
clientSecret,
|
|
1793
1866
|
cpuTarget: cpuChallengeDetails.target,
|
|
1794
|
-
memDifficulty: memDifficulty
|
|
1867
|
+
memDifficulty: memDifficulty,
|
|
1868
|
+
originalPath: path, // *** FIX: Store the original path ***
|
|
1795
1869
|
}, this.securityConfig.challengeTtl || 300); // NOUVEAU: TTL configurable (5min par défaut)
|
|
1796
1870
|
|
|
1797
1871
|
// Associate the current challenge nonce with the device for trap URL verification later.
|
|
@@ -1811,7 +1885,7 @@ export class FingerprintEngine {
|
|
|
1811
1885
|
}
|
|
1812
1886
|
|
|
1813
1887
|
// Check if the request is an API request to return a JSON challenge
|
|
1814
|
-
const isApi = this.securityConfig.thresholds?.isApiRequest?.(requestContext);
|
|
1888
|
+
const isApi = requestContext.rawReq && this.securityConfig.thresholds?.isApiRequest?.(requestContext.rawReq);
|
|
1815
1889
|
|
|
1816
1890
|
if (isApi) {
|
|
1817
1891
|
// For API clients, send a JSON response with challenge details.
|
|
@@ -1828,8 +1902,7 @@ export class FingerprintEngine {
|
|
|
1828
1902
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
|
|
1829
1903
|
} else {
|
|
1830
1904
|
// For browsers, send the HTML page.
|
|
1831
|
-
const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`;
|
|
1832
|
-
const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret).replace('</body>', `${trapContainer}</body>`);
|
|
1905
|
+
const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer);
|
|
1833
1906
|
this._log('Browser challenge page generated', {
|
|
1834
1907
|
pageLength: page.length,
|
|
1835
1908
|
hasTrapContainer: true
|
|
@@ -2090,7 +2163,6 @@ export const powMiddleware = (securityConfig) => {
|
|
|
2090
2163
|
*/
|
|
2091
2164
|
export const __internal = {
|
|
2092
2165
|
getDeviceHash,
|
|
2093
|
-
isMalicious,
|
|
2094
2166
|
getSuspicionVector,
|
|
2095
2167
|
cyrb53, // Export for testing
|
|
2096
2168
|
FingerprintBuilder, // Export for testing
|
package/package.json
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
|
|
5
|
-
"main": "fingerprint.js",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"engines": {
|
|
8
|
-
"node": ">=20.0.0"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"test": "vitest run --reporter=verbose"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"fingerprint.js",
|
|
15
|
-
"fingerprint.client.js",
|
|
16
|
-
"fingerprint.builder.js",
|
|
17
|
-
"pow.solver.js",
|
|
18
|
-
"library.js",
|
|
19
|
-
"redis-store.js",
|
|
20
|
-
"mongodb-store.js",
|
|
21
|
-
"sql-store.js",
|
|
22
|
-
"README.md",
|
|
23
|
-
"LICENSE"
|
|
24
|
-
],
|
|
25
|
-
"repository": {
|
|
26
|
-
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/anonympins/fingerprint.git"
|
|
28
|
-
},
|
|
29
|
-
"keywords": [
|
|
30
|
-
"fingerprint",
|
|
31
|
-
"bot",
|
|
32
|
-
"anti-bot",
|
|
33
|
-
"security",
|
|
34
|
-
"express",
|
|
35
|
-
"middleware",
|
|
36
|
-
"proof-of-work",
|
|
37
|
-
"pow",
|
|
38
|
-
"rate-limiting",
|
|
39
|
-
"mitigation",
|
|
40
|
-
"captcha",
|
|
41
|
-
"dns"
|
|
42
|
-
],
|
|
43
|
-
"author": "anonympins",
|
|
44
|
-
"license": "MIT",
|
|
45
|
-
"bugs": {
|
|
46
|
-
"url": "https://github.com/anonympins/fingerprint/issues"
|
|
47
|
-
},
|
|
48
|
-
"homepage": "https://github.com/anonympins/fingerprint#readme",
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"body-parser": "^1.20.2",
|
|
51
|
-
"cookie-parser": "^1.4.6",
|
|
52
|
-
"express": "^4.18.2",
|
|
53
|
-
"prom-client": "^15.1.2",
|
|
54
|
-
"vitest": "^4.1.11"
|
|
55
|
-
},
|
|
56
|
-
"peerDependencies": {
|
|
57
|
-
"ioredis": "^5.3.2",
|
|
58
|
-
"knex": ">=3.0.0",
|
|
59
|
-
"mongodb": "^6.3.0",
|
|
60
|
-
"sqlite3": "^5.1.7"
|
|
61
|
-
},
|
|
62
|
-
"peerDependenciesMeta": {
|
|
63
|
-
"ioredis": {
|
|
64
|
-
"optional": true
|
|
65
|
-
},
|
|
66
|
-
"mongodb": {
|
|
67
|
-
"optional": true
|
|
68
|
-
},
|
|
69
|
-
"knex": {
|
|
70
|
-
"optional": true
|
|
71
|
-
},
|
|
72
|
-
"sqlite3": {
|
|
73
|
-
"optional": true
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@anonympins/fingerprint",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
|
|
5
|
+
"main": "fingerprint.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20.0.0"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "vitest run --reporter=verbose"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"fingerprint.js",
|
|
15
|
+
"fingerprint.client.js",
|
|
16
|
+
"fingerprint.builder.js",
|
|
17
|
+
"pow.solver.js",
|
|
18
|
+
"library.js",
|
|
19
|
+
"redis-store.js",
|
|
20
|
+
"mongodb-store.js",
|
|
21
|
+
"sql-store.js",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/anonympins/fingerprint.git"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"fingerprint",
|
|
31
|
+
"bot",
|
|
32
|
+
"anti-bot",
|
|
33
|
+
"security",
|
|
34
|
+
"express",
|
|
35
|
+
"middleware",
|
|
36
|
+
"proof-of-work",
|
|
37
|
+
"pow",
|
|
38
|
+
"rate-limiting",
|
|
39
|
+
"mitigation",
|
|
40
|
+
"captcha",
|
|
41
|
+
"dns"
|
|
42
|
+
],
|
|
43
|
+
"author": "anonympins",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/anonympins/fingerprint/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/anonympins/fingerprint#readme",
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"body-parser": "^1.20.2",
|
|
51
|
+
"cookie-parser": "^1.4.6",
|
|
52
|
+
"express": "^4.18.2",
|
|
53
|
+
"prom-client": "^15.1.2",
|
|
54
|
+
"vitest": "^4.1.11"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"ioredis": "^5.3.2",
|
|
58
|
+
"knex": ">=3.0.0",
|
|
59
|
+
"mongodb": "^6.3.0",
|
|
60
|
+
"sqlite3": "^5.1.7"
|
|
61
|
+
},
|
|
62
|
+
"peerDependenciesMeta": {
|
|
63
|
+
"ioredis": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"mongodb": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"knex": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"sqlite3": {
|
|
73
|
+
"optional": true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
package/pow.solver.js
CHANGED
|
@@ -1,214 +1,224 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file @/pow.solver.js
|
|
3
|
-
* @description Contient les fonctions côté client pour résoudre les différents types de challenges Proof-of-Work.
|
|
4
|
-
* Fichier compatible à la fois avec l'import de modules ES6 et l'injection directe dans un script HTML.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
'use strict';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Résout un challenge CPU basé sur une cible (version inline pour compatibilité HTML).
|
|
11
|
-
* @param {string} clientIp - L'adresse IP du client.
|
|
12
|
-
* @param {string} nonce - Le nonce du challenge.
|
|
13
|
-
* @param {bigint} target - La cible à atteindre.
|
|
14
|
-
* @param {string} clientSecret - Le secret client (optionnel).
|
|
15
|
-
* @param {Function} progressCallback - Callback pour les mises à jour de progression.
|
|
16
|
-
* @returns {Promise<number>} La solution (un nombre entier).
|
|
17
|
-
*/
|
|
18
|
-
export async function solveCpuTargetInline(clientIp, nonce, target, clientSecret = null, progressCallback) {
|
|
19
|
-
const cpuTarget = BigInt(target);
|
|
20
|
-
let cpuSolution = 0;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*
|
|
40
|
-
* @param {
|
|
41
|
-
* @
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
worker
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
*
|
|
75
|
-
* @param {
|
|
76
|
-
* @
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* @param {number}
|
|
101
|
-
* @
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
function distance
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
let
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
let
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
*
|
|
162
|
-
* @
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file @/pow.solver.js
|
|
3
|
+
* @description Contient les fonctions côté client pour résoudre les différents types de challenges Proof-of-Work.
|
|
4
|
+
* Fichier compatible à la fois avec l'import de modules ES6 et l'injection directe dans un script HTML.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Résout un challenge CPU basé sur une cible (version inline pour compatibilité HTML).
|
|
11
|
+
* @param {string} clientIp - L'adresse IP du client.
|
|
12
|
+
* @param {string} nonce - Le nonce du challenge.
|
|
13
|
+
* @param {bigint} target - La cible à atteindre.
|
|
14
|
+
* @param {string} clientSecret - Le secret client (optionnel).
|
|
15
|
+
* @param {Function} progressCallback - Callback pour les mises à jour de progression.
|
|
16
|
+
* @returns {Promise<number>} La solution (un nombre entier).
|
|
17
|
+
*/
|
|
18
|
+
export async function solveCpuTargetInline(clientIp, nonce, target, clientSecret = null, progressCallback) {
|
|
19
|
+
const cpuTarget = BigInt('0x' + target);
|
|
20
|
+
let cpuSolution = 0;
|
|
21
|
+
const ipPart = clientIp || ''; // Use empty string if IP is null/undefined
|
|
22
|
+
while (true) { // When a clientSecret is used, the IP is omitted from the hash to make it independent of the network.
|
|
23
|
+
const msg = clientSecret ?
|
|
24
|
+
`${nonce}:${cpuSolution}:${clientSecret}` :
|
|
25
|
+
`${ipPart}:${nonce}:${cpuSolution}`;
|
|
26
|
+
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
|
|
27
|
+
const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
28
|
+
if (BigInt('0x' + hashHex) < cpuTarget) break;
|
|
29
|
+
cpuSolution++;
|
|
30
|
+
if (cpuSolution % 100000 === 0) {
|
|
31
|
+
await new Promise(r => setTimeout(r, 0));
|
|
32
|
+
if (progressCallback) progressCallback(cpuSolution);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return cpuSolution;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Résout un challenge CPU basé sur une cible (version Web Worker).
|
|
40
|
+
* @param {string} message - Le message à hasher (ex: `ip:nonce:solution:secret`).
|
|
41
|
+
* @param {bigint} target - La cible à atteindre.
|
|
42
|
+
* @returns {Promise<number>} La solution (un nombre entier).
|
|
43
|
+
*/
|
|
44
|
+
export async function solveCpuTarget(message, target) {
|
|
45
|
+
// Vérifie si les Web Workers sont supportés par le navigateur.
|
|
46
|
+
if (typeof(Worker) === "undefined") {
|
|
47
|
+
console.warn("Web Workers not supported. Falling back to main thread calculation (UI may freeze).");
|
|
48
|
+
// Ici, on pourrait remettre l'ancienne implémentation comme solution de secours.
|
|
49
|
+
// Pour la clarté, nous supposons que les workers sont disponibles.
|
|
50
|
+
throw new Error("Web Worker support is required for CPU challenges.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
// Crée un worker à partir du script dédié. Le chemin doit être accessible publiquement.
|
|
55
|
+
// Assurez-vous que `pow.worker.js` est servi par votre serveur statique.
|
|
56
|
+
const worker = new Worker('./pow.worker.js');
|
|
57
|
+
|
|
58
|
+
worker.onmessage = (event) => {
|
|
59
|
+
resolve(event.data.solution);
|
|
60
|
+
worker.terminate(); // Nettoie le worker une fois le travail terminé.
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
worker.onerror = (error) => {
|
|
64
|
+
reject(error);
|
|
65
|
+
worker.terminate();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Envoie les données du challenge au worker pour qu'il commence le calcul.
|
|
69
|
+
worker.postMessage({ message, target });
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Résout un challenge basé sur la mémoire.
|
|
75
|
+
* @param {string} seed - La graine pour l'initialisation de la mémoire.
|
|
76
|
+
* @param {number} difficulty - La difficulté (en Mo).
|
|
77
|
+
* @returns {Promise<number>} La solution (nombre entier).
|
|
78
|
+
*/
|
|
79
|
+
export async function solveMemory(seed, difficulty) {
|
|
80
|
+
const size = difficulty * 1024 * 1024;
|
|
81
|
+
const buffer = new Uint32Array(size / 4);
|
|
82
|
+
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
83
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
84
|
+
buffer[i] = (h = Math.imul(h ^ i, 1597334677));
|
|
85
|
+
}
|
|
86
|
+
let solution = 0;
|
|
87
|
+
const iterations = size / 16;
|
|
88
|
+
let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
|
|
89
|
+
for (let i = 0; i < iterations; i++) {
|
|
90
|
+
addr = buffer[addr] % buffer.length;
|
|
91
|
+
solution ^= addr;
|
|
92
|
+
}
|
|
93
|
+
return solution;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Résout un challenge de type "Problème du Voyageur de Commerce" (TSP).
|
|
98
|
+
* NOTE: Ceci est une implémentation simple (heuristique du plus proche voisin) et n'est pas garantie
|
|
99
|
+
* de trouver la solution optimale, mais elle est suffisante pour un challenge.
|
|
100
|
+
* @param {Array<{x: number, y: number}>} cities - Les coordonnées des villes.
|
|
101
|
+
* @param {number} targetMaxDistance - La distance maximale acceptable.
|
|
102
|
+
* @returns {Promise<{path: number[], distance: number}>} Le chemin et la distance.
|
|
103
|
+
*/
|
|
104
|
+
export async function solveTsp(cities, targetMaxDistance) {
|
|
105
|
+
// Utility function to calculate the distance between two cities
|
|
106
|
+
function distance(city1, city2) {
|
|
107
|
+
return Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Utility function to evaluate the total distance of a path
|
|
111
|
+
function evaluatePathDistance(cities, path) {
|
|
112
|
+
let totalDistance = 0;
|
|
113
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
114
|
+
totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
|
|
115
|
+
}
|
|
116
|
+
totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Return to start
|
|
117
|
+
return totalDistance;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Solveur simple du TSP (heuristique du plus proche voisin)
|
|
121
|
+
function solveTspNearestNeighbor(cities) {
|
|
122
|
+
const numCities = cities.length;
|
|
123
|
+
if (numCities === 0) return [];
|
|
124
|
+
|
|
125
|
+
let currentPath = [];
|
|
126
|
+
let visited = new Array(numCities).fill(false);
|
|
127
|
+
|
|
128
|
+
let currentCityIndex = 0; // Always start with the first city for reproducibility
|
|
129
|
+
currentPath.push(currentCityIndex);
|
|
130
|
+
visited[currentCityIndex] = true;
|
|
131
|
+
|
|
132
|
+
for (let i = 1; i < numCities; i++) {
|
|
133
|
+
let nearestCityIndex = -1;
|
|
134
|
+
let minDistance = Infinity;
|
|
135
|
+
|
|
136
|
+
for (let j = 0; j < numCities; j++) {
|
|
137
|
+
if (!visited[j]) {
|
|
138
|
+
const dist = distance(cities[currentCityIndex], cities[j]);
|
|
139
|
+
if (dist < minDistance) {
|
|
140
|
+
minDistance = dist;
|
|
141
|
+
nearestCityIndex = j;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
currentCityIndex = nearestCityIndex;
|
|
146
|
+
currentPath.push(currentCityIndex);
|
|
147
|
+
visited[currentCityIndex] = true;
|
|
148
|
+
}
|
|
149
|
+
return currentPath;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// To avoid freezing the browser, yield the thread from time to time
|
|
153
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
154
|
+
const solutionPath = solveTspNearestNeighbor(cities);
|
|
155
|
+
const solutionDistance = evaluatePathDistance(cities, solutionPath);
|
|
156
|
+
|
|
157
|
+
return { path: solutionPath, distance: solutionDistance };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Fonction principale qui reçoit un objet challenge et le résout.
|
|
162
|
+
* @param {object} challenge - L'objet challenge reçu du serveur.
|
|
163
|
+
* @returns {Promise<object>} Un objet contenant la ou les solutions.
|
|
164
|
+
*/
|
|
165
|
+
export async function solveChallenge(challenge) {
|
|
166
|
+
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
|
|
167
|
+
const solutions = {};
|
|
168
|
+
|
|
169
|
+
switch (type) {
|
|
170
|
+
case 'cpu_target':
|
|
171
|
+
const baseMessageCpu = `:${nonce}`; // L'IP est gérée côté serveur
|
|
172
|
+
if (!cpuTarget) {
|
|
173
|
+
throw new Error("Challenge data is missing 'cpuTarget' property.");
|
|
174
|
+
}
|
|
175
|
+
const target = cpuTarget; // Keep variable name for consistency below
|
|
176
|
+
solutions.cpu = await solveCpuTarget(baseMessageCpu, BigInt('0x' + target));
|
|
177
|
+
break;
|
|
178
|
+
case 'cpu_mem':
|
|
179
|
+
// Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
|
|
180
|
+
const baseMessageCombined = `${nonce}:${clientSecret}`;
|
|
181
|
+
const memSeed = `:${nonce}:${clientSecret}`;
|
|
182
|
+
const [cpuSol, memSol] = await Promise.all([
|
|
183
|
+
(async () => {
|
|
184
|
+
if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property.");
|
|
185
|
+
return solveCpuTargetInline(null, nonce, cpuTarget, clientSecret);
|
|
186
|
+
})(),
|
|
187
|
+
solveMemory(memSeed, memDifficulty)
|
|
188
|
+
]);
|
|
189
|
+
solutions.cpu = cpuSol;
|
|
190
|
+
solutions.mem = memSol;
|
|
191
|
+
break;
|
|
192
|
+
case 'cpu_mem_inline':
|
|
193
|
+
// Version inline pour compatibilité HTML avec IP incluse
|
|
194
|
+
const memSeedInline = `${nonce}:${clientSecret}`;
|
|
195
|
+
const [cpuSolInline, memSolInline] = await Promise.all([
|
|
196
|
+
(async () => {
|
|
197
|
+
if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property.");
|
|
198
|
+
return solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret);
|
|
199
|
+
})(),
|
|
200
|
+
solveMemory(memSeedInline, memDifficulty)
|
|
201
|
+
]);
|
|
202
|
+
solutions.cpu = cpuSolInline;
|
|
203
|
+
solutions.mem = memSolInline;
|
|
204
|
+
break;
|
|
205
|
+
case 'tsp':
|
|
206
|
+
const tspResult = await solveTsp(cities, targetMaxDistance);
|
|
207
|
+
solutions.tsp = tspResult.path;
|
|
208
|
+
solutions.distance = tspResult.distance;
|
|
209
|
+
break;
|
|
210
|
+
default:
|
|
211
|
+
throw new Error(`Unknown challenge type: ${type}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return solutions;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- Compatibilité pour l'injection directe dans le HTML ---
|
|
218
|
+
// Si le script est chargé dans un navigateur (window existe), on attache les fonctions nécessaires à window.
|
|
219
|
+
if (typeof window !== 'undefined') {
|
|
220
|
+
window.solveCpuChallengeInline = solveCpuTargetInline;
|
|
221
|
+
window.solveMemoryChallenge = solveMemory;
|
|
222
|
+
window.solveTspChallenge = solveTsp;
|
|
223
|
+
window.solveChallenge = solveChallenge;
|
|
214
224
|
}
|