@anonympins/fingerprint 0.2.0 → 0.2.1
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 +4 -0
- package/fingerprint.builder.js +160 -103
- package/fingerprint.client.js +10 -9
- package/fingerprint.js +163 -170
- package/optimization.worker.js +28 -0
- package/package.json +4 -1
- package/pow.solver.js +41 -21
- package/pow.worker.js +27 -0
- package/problem-manager.js +175 -0
package/README.md
CHANGED
|
@@ -98,6 +98,10 @@ const securityConfig = {
|
|
|
98
98
|
high: 75, // Score for a very difficult challenge
|
|
99
99
|
block: 95, // Score above which the request is blocked outright (HTTP 404)
|
|
100
100
|
},
|
|
101
|
+
cpu: {
|
|
102
|
+
minDifficultyBits: 8,
|
|
103
|
+
maxDifficultyBits: 24,
|
|
104
|
+
},
|
|
101
105
|
// (Optional) Configure the duration (in milliseconds) for various temporary data.
|
|
102
106
|
ticketMaxAge: 3600000, // 1 hour. Duration for which a solved challenge ticket is valid.
|
|
103
107
|
challengeTtl: 300000, // 5 minutes. Time during which a challenge nonce is valid.
|
package/fingerprint.builder.js
CHANGED
|
@@ -1,104 +1,161 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Algorithme de hachage cyrb53 (rapide et faible taux de collision).
|
|
3
|
-
*/
|
|
4
|
-
export const cyrb53 = (str, seed = 0) => {
|
|
5
|
-
let h1 = 0xdeadbeef ^ seed,
|
|
6
|
-
h2 = 0x41c6ce57 ^ seed;
|
|
7
|
-
for (let i = 0, ch; i < str.length; i++) {
|
|
8
|
-
ch = str.charCodeAt(i);
|
|
9
|
-
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
10
|
-
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
11
|
-
}
|
|
12
|
-
h1 =
|
|
13
|
-
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
|
|
14
|
-
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
15
|
-
h2 =
|
|
16
|
-
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
|
|
17
|
-
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
18
|
-
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Classe pour construire une empreinte composite (Multi-Hash).
|
|
23
|
-
* Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
|
|
24
|
-
*/
|
|
25
|
-
export class FingerprintBuilder {
|
|
26
|
-
constructor() {
|
|
27
|
-
this.components = new Map();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Ajoute un composant au hash global.
|
|
32
|
-
* @param {string} group - Le nom du groupe (ex: 'hw', 'screen', 'geo')
|
|
33
|
-
* @param {string|number|boolean} value - La valeur brute à hasher
|
|
34
|
-
*/
|
|
35
|
-
add(group, value) {
|
|
36
|
-
if (value === undefined || value === null) return this;
|
|
37
|
-
// On hash la valeur individuellement pour l'anonymiser et réduire sa taille
|
|
38
|
-
this.components.set(group, cyrb53(String(value)));
|
|
39
|
-
return this;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Adds a raw component without hashing it.
|
|
44
|
-
* Useful for metrics that need to be read on the server.
|
|
45
|
-
* @param {string} group - The name of the group.
|
|
46
|
-
* @param {string|number} value - The raw value.
|
|
47
|
-
*/
|
|
48
|
-
addRaw(group, value) {
|
|
49
|
-
if (value === undefined || value === null) return this;
|
|
50
|
-
this.components.set(group, value);
|
|
51
|
-
return this;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Algorithme de hachage cyrb53 (rapide et faible taux de collision).
|
|
3
|
+
*/
|
|
4
|
+
export const cyrb53 = (str, seed = 0) => {
|
|
5
|
+
let h1 = 0xdeadbeef ^ seed,
|
|
6
|
+
h2 = 0x41c6ce57 ^ seed;
|
|
7
|
+
for (let i = 0, ch; i < str.length; i++) {
|
|
8
|
+
ch = str.charCodeAt(i);
|
|
9
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
10
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
11
|
+
}
|
|
12
|
+
h1 =
|
|
13
|
+
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
|
|
14
|
+
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
15
|
+
h2 =
|
|
16
|
+
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
|
|
17
|
+
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
18
|
+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Classe pour construire une empreinte composite (Multi-Hash).
|
|
23
|
+
* Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
|
|
24
|
+
*/
|
|
25
|
+
export class FingerprintBuilder {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.components = new Map();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Ajoute un composant au hash global.
|
|
32
|
+
* @param {string} group - Le nom du groupe (ex: 'hw', 'screen', 'geo')
|
|
33
|
+
* @param {string|number|boolean} value - La valeur brute à hasher
|
|
34
|
+
*/
|
|
35
|
+
add(group, value) {
|
|
36
|
+
if (value === undefined || value === null) return this;
|
|
37
|
+
// On hash la valeur individuellement pour l'anonymiser et réduire sa taille
|
|
38
|
+
this.components.set(group, cyrb53(String(value)));
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Adds a raw component without hashing it.
|
|
44
|
+
* Useful for metrics that need to be read on the server.
|
|
45
|
+
* @param {string} group - The name of the group.
|
|
46
|
+
* @param {string|number} value - The raw value.
|
|
47
|
+
*/
|
|
48
|
+
addRaw(group, value) {
|
|
49
|
+
if (value === undefined || value === null) return this;
|
|
50
|
+
this.components.set(group, value);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Affiche les composants actuels dans la console.
|
|
56
|
+
* @param {string} [title='FingerprintBuilder Components'] - Un titre pour le log.
|
|
57
|
+
*/
|
|
58
|
+
log(title = 'FingerprintBuilder Components') {
|
|
59
|
+
console.log(`--- ${title} ---`);
|
|
60
|
+
const sortedComponents = Array.from(this.components.entries())
|
|
61
|
+
.sort((a, b) => a[0].localeCompare(b[0]));
|
|
62
|
+
|
|
63
|
+
console.table(Object.fromEntries(sortedComponents));
|
|
64
|
+
console.log(`Final string: ${this.toString()}`);
|
|
65
|
+
console.log(`---------------------------------${'-'.repeat(title.length)}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Génère la chaîne de signature finale.
|
|
70
|
+
* Trie les clés pour garantir un ordre déterministe.
|
|
71
|
+
*/
|
|
72
|
+
toString() {
|
|
73
|
+
return Array.from(this.components.entries())
|
|
74
|
+
.sort((a, b) => a[0].localeCompare(b[0])) // Tri alphabétique des clés
|
|
75
|
+
.map(([key, hash]) => `${key}:${hash}`)
|
|
76
|
+
.join("|");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Adds a raw component without hashing it.
|
|
80
|
+
* Useful for metrics that need to be read on the server.
|
|
81
|
+
* @param {string} group - The name of the group.
|
|
82
|
+
* @param {string|number} value - The raw value.
|
|
83
|
+
*/
|
|
84
|
+
addRaw(group, value) {
|
|
85
|
+
if (value === undefined || value === null) return this;
|
|
86
|
+
this.components.set(group, value);
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Compares two fingerprints and returns a similarity score (0 to 1).
|
|
92
|
+
* Uses weights to give more importance to strong invariants (Canvas, GPU).
|
|
93
|
+
* @param {string} fpString1 - Fingerprint A
|
|
94
|
+
* @param {string} fpString2 - Fingerprint B
|
|
95
|
+
*/
|
|
96
|
+
static compare(fpString1, fpString2) {
|
|
97
|
+
if (!fpString1 || !fpString2) return 0;
|
|
98
|
+
|
|
99
|
+
const parse = (str) => new Map(str.split("|").map(part => part.split(":")).filter(([k,v]) => k && v));
|
|
100
|
+
|
|
101
|
+
const map1 = parse(fpString1);
|
|
102
|
+
const map2 = parse(fpString2);
|
|
103
|
+
|
|
104
|
+
// Keys to ignore when comparing the initial request fingerprint with the challenge solver's fingerprint.
|
|
105
|
+
// Headers like Client-Hints (ch_*), cookie presence (cookie_keys), and upgrade-insecure-requests
|
|
106
|
+
// can vary or be absent on the subsequent request that submits the solution, especially after a redirect.
|
|
107
|
+
// By ignoring them, we focus the comparison on more stable identifiers like UA, JA3, GPU, etc.
|
|
108
|
+
const volatileKeys = new Set([
|
|
109
|
+
'ch_ua', 'ch_platform', 'ch_mobile', 'ch_model', 'ch_arch', 'ch_bitness',
|
|
110
|
+
'cookie_keys', 'upgrade',
|
|
111
|
+
// Also ignore network and http version as they can change between requests (e.g., proxy, protocol upgrade)
|
|
112
|
+
'network', 'http_ver',
|
|
113
|
+
// Ignore proxy-related headers as they are not stable client identifiers
|
|
114
|
+
'x_forwarded_for', 'x_real_ip', 'cf_connecting_ip'
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
// Poids de "véracité" (Entropie/Stabilité)
|
|
118
|
+
// Les poids sont augmentés pour donner plus d'importance aux signaux forts.
|
|
119
|
+
const weights = {
|
|
120
|
+
// --- Signaux très forts (difficiles à usurper) ---
|
|
121
|
+
cvs: 5.0, // Canvas: Très haute entropie (Rendu unique du GPU/driver)
|
|
122
|
+
gpu: 4.0, // GPU: Haute entropie (Matériel spécifique)
|
|
123
|
+
ja3: 3.5, // JA3: Identifie la librairie TLS (très stable pour un client donné)
|
|
124
|
+
ua: 2.0, // User-Agent: Signal fort, bien que modifiable
|
|
125
|
+
|
|
126
|
+
// --- Signaux composites et dérivés ---
|
|
127
|
+
client_fp_hash: 3.0, // Le hash de l'empreinte client est un signal très fort.
|
|
128
|
+
browser: 1.5, // Le navigateur extrait du UA.
|
|
129
|
+
os_version: 1.5, // L'OS extrait du UA.
|
|
130
|
+
device_type: 1.0, // Le type d'appareil extrait du UA.
|
|
131
|
+
|
|
132
|
+
// --- Signaux moyens ---
|
|
133
|
+
hw: 1.5, // Hardware (CPU, RAM): Stabilité moyenne
|
|
134
|
+
scr: 1.0, // Screen: Stabilité moyenne
|
|
135
|
+
// 'os' est souvent la même chose que 'ch_platform', on peut le déprécier ou lui donner un poids faible.
|
|
136
|
+
os: 0.8, // OS (nav.platform): Assez stable
|
|
137
|
+
geo: 0.5, // Geo/Langue: Peut changer (VPN, voyage)
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
let weightedMatches = 0;
|
|
141
|
+
let totalWeight = 0;
|
|
142
|
+
|
|
143
|
+
const allKeys = new Set([...map1.keys(), ...map2.keys()]);
|
|
144
|
+
|
|
145
|
+
allKeys.forEach((key) => {
|
|
146
|
+
// On ignore les clés volatiles pour cette comparaison spécifique.
|
|
147
|
+
if (volatileKeys.has(key)) return;
|
|
148
|
+
|
|
149
|
+
// On ne compare que les clés qui ont un poids défini.
|
|
150
|
+
const weight = weights[key];
|
|
151
|
+
if (!weight) return;
|
|
152
|
+
|
|
153
|
+
totalWeight += weight; // N'incrémenter que si la clé est pertinente.
|
|
154
|
+
if (map1.get(key) === map2.get(key)) {
|
|
155
|
+
weightedMatches += weight;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return totalWeight === 0 ? 0 : weightedMatches / totalWeight;
|
|
160
|
+
}
|
|
104
161
|
}
|
package/fingerprint.client.js
CHANGED
|
@@ -335,7 +335,7 @@ const ClientLibrary = {
|
|
|
335
335
|
* @private
|
|
336
336
|
*/
|
|
337
337
|
async solveChallengeAndRetry(response, resource, options) {
|
|
338
|
-
if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json')) {
|
|
338
|
+
if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json') || response.bodyUsed) {
|
|
339
339
|
return response;
|
|
340
340
|
}
|
|
341
341
|
|
|
@@ -346,7 +346,9 @@ const ClientLibrary = {
|
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
|
|
349
|
-
|
|
349
|
+
// L'empreinte de l'appareil qui résout le challenge est cruciale.
|
|
350
|
+
const solverFp = this.getDeviceFingerprint();
|
|
351
|
+
const solution = await solveChallenge(challengeData.challenge, solverFp);
|
|
350
352
|
console.log('[Fingerprint] Challenge solved. Retrying original request.');
|
|
351
353
|
|
|
352
354
|
// Ajouter la solution aux paramètres de la requête pour le nouvel essai
|
|
@@ -360,17 +362,15 @@ const ClientLibrary = {
|
|
|
360
362
|
url.searchParams.set(`pow_solution_${key}`, String(value));
|
|
361
363
|
});
|
|
362
364
|
|
|
363
|
-
// Pour le challenge d'optimisation, la solution est un tableau d'objets
|
|
364
|
-
if (solution.population) {
|
|
365
|
-
url.searchParams.set('pow_solution_population', JSON.stringify(solution.population));
|
|
366
|
-
}
|
|
367
|
-
|
|
368
365
|
// Pour le challenge de travail utile
|
|
369
366
|
if (solution.work_result) {
|
|
370
367
|
url.searchParams.set('pow_solution_work_result', JSON.stringify(solution.work_result));
|
|
371
368
|
url.searchParams.set('pow_problem_id', solution.problem_id);
|
|
372
369
|
}
|
|
373
370
|
|
|
371
|
+
// On ajoute l'empreinte du solveur à la requête de réessai.
|
|
372
|
+
url.searchParams.set('pow_fp', solverFp);
|
|
373
|
+
|
|
374
374
|
// On utilise la chaîne d'intercepteurs pour la requête réessayée,
|
|
375
375
|
// ce qui garantit que le fetch original est appelé avec le bon contexte.
|
|
376
376
|
// Cela évite de réintroduire l'erreur "Illegal invocation".
|
|
@@ -418,8 +418,9 @@ const ClientLibrary = {
|
|
|
418
418
|
// Ajoute l'intercepteur pour la résolution de challenge
|
|
419
419
|
if (fetchConfig.handleChallenges !== false) {
|
|
420
420
|
this.addFetchInterceptor(async (resource, options, next) => {
|
|
421
|
-
const
|
|
422
|
-
|
|
421
|
+
const originalResponse = await next(resource, options);
|
|
422
|
+
// On clone la réponse pour que la lecture du corps par solveChallengeAndRetry ne la consomme pas pour l'appelant original.
|
|
423
|
+
return this.solveChallengeAndRetry(originalResponse.clone(), resource, options);
|
|
423
424
|
});
|
|
424
425
|
}
|
|
425
426
|
}
|
package/fingerprint.js
CHANGED
|
@@ -27,111 +27,12 @@ const getPowSecret = () => {
|
|
|
27
27
|
* @returns {string} The solver JavaScript code.
|
|
28
28
|
*/
|
|
29
29
|
const getPowSolverCode = () => {
|
|
30
|
-
try
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
console.warn('Could not load pow.solver.js for inlining, using fallback inline code');
|
|
37
|
-
// Fallback inline code if file cannot be loaded
|
|
38
|
-
return `(function(global){
|
|
39
|
-
async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){
|
|
40
|
-
const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
|
|
41
|
-
let cpuSolution = 0;
|
|
42
|
-
const ipPart = clientIp || '';
|
|
43
|
-
while(true){
|
|
44
|
-
// When a clientSecret is used, the IP is omitted from the hash to make it independent of the network.
|
|
45
|
-
const msg = clientSecret ? \`\${nonce}:\${cpuSolution}:\${clientSecret}\` : \`\${ipPart}:\${nonce}:\${cpuSolution}\`;
|
|
46
|
-
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
|
|
47
|
-
const hashHex = Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join('');
|
|
48
|
-
if(BigInt('0x'+hashHex) < cpuTarget) break;
|
|
49
|
-
cpuSolution++;
|
|
50
|
-
if(cpuSolution % 100000 === 0) await new Promise(r=>setTimeout(r,0));
|
|
51
|
-
}
|
|
52
|
-
return cpuSolution;
|
|
53
|
-
}
|
|
54
|
-
async function solveMemory(seed, difficulty){
|
|
55
|
-
const size = difficulty * 1024 * 1024;
|
|
56
|
-
const buffer = new Uint32Array(size / 4);
|
|
57
|
-
let h = new TextEncoder().encode(seed).reduce((acc,v)=>acc+v,0);
|
|
58
|
-
for(let i=0;i<buffer.length;i++) buffer[i] = h = Math.imul(h^i,1597334677);
|
|
59
|
-
let solution = 0;
|
|
60
|
-
const iterations = size / 16;
|
|
61
|
-
let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
|
|
62
|
-
for(let i=0;i<iterations;i++){
|
|
63
|
-
addr = buffer[addr] % buffer.length;
|
|
64
|
-
solution ^= addr;
|
|
65
|
-
}
|
|
66
|
-
return solution;
|
|
67
|
-
}
|
|
68
|
-
async function solveTsp(cities, targetMaxDistance){
|
|
69
|
-
function distance(c1,c2){return Math.sqrt(Math.pow(c1.x-c2.x,2)+Math.pow(c1.y-c2.y,2));}
|
|
70
|
-
function evaluatePathDistance(cities,path){
|
|
71
|
-
let total=0;
|
|
72
|
-
for(let i=0;i<path.length-1;i++) total+=distance(cities[path[i]],cities[path[i+1]]);
|
|
73
|
-
total+=distance(cities[path[path.length-1]],cities[path[0]]);
|
|
74
|
-
return total;
|
|
75
|
-
}
|
|
76
|
-
function solveTspNearestNeighbor(cities){
|
|
77
|
-
const n=cities.length;
|
|
78
|
-
if(n===0)return[];
|
|
79
|
-
let path=[0];
|
|
80
|
-
let visited=new Array(n).fill(false);
|
|
81
|
-
visited[0]=true;
|
|
82
|
-
for(let i=1;i<n;i++){
|
|
83
|
-
let nearest=-1, minDist=Infinity;
|
|
84
|
-
for(let j=0;j<n;j++){
|
|
85
|
-
if(!visited[j]){
|
|
86
|
-
const d=distance(cities[path[i-1]],cities[j]);
|
|
87
|
-
if(d<minDist){minDist=d;nearest=j;}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
path.push(nearest);
|
|
91
|
-
visited[nearest]=true;
|
|
92
|
-
}
|
|
93
|
-
return path;
|
|
94
|
-
}
|
|
95
|
-
await new Promise(r=>setTimeout(r,10));
|
|
96
|
-
const solutionPath=solveTspNearestNeighbor(cities);
|
|
97
|
-
const solutionDistance=evaluatePathDistance(cities,solutionPath);
|
|
98
|
-
return{path:solutionPath,distance:solutionDistance};
|
|
99
|
-
}
|
|
100
|
-
async function solveChallenge(challenge) {
|
|
101
|
-
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
|
|
102
|
-
const solutions = {};
|
|
103
|
-
|
|
104
|
-
switch (type) {
|
|
105
|
-
case 'cpu_target':
|
|
106
|
-
solutions.cpu = await solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret);
|
|
107
|
-
break;
|
|
108
|
-
case 'cpu_mem':
|
|
109
|
-
case 'cpu_mem_inline':
|
|
110
|
-
const memSeed = nonce + ":" + clientSecret;
|
|
111
|
-
const [cpuSol, memSol] = await Promise.all([
|
|
112
|
-
solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret),
|
|
113
|
-
solveMemory(memSeed, memDifficulty)
|
|
114
|
-
]);
|
|
115
|
-
solutions.cpu = cpuSol;
|
|
116
|
-
solutions.mem = memSol;
|
|
117
|
-
break;
|
|
118
|
-
case 'tsp':
|
|
119
|
-
const tspResult = await solveTsp(cities, targetMaxDistance);
|
|
120
|
-
solutions.tsp = tspResult.path;
|
|
121
|
-
solutions.distance = tspResult.distance;
|
|
122
|
-
break;
|
|
123
|
-
default:
|
|
124
|
-
throw new Error(\`Unknown challenge type: \${type}\`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return solutions;
|
|
128
|
-
}
|
|
129
|
-
global.solveCpuChallengeInline=solveCpuTargetInline;
|
|
130
|
-
global.solveMemoryChallenge=solveMemory;
|
|
131
|
-
global.solveTspChallenge=solveTsp;
|
|
132
|
-
global.solveChallenge=solveChallenge;
|
|
133
|
-
})(typeof window!=='undefined'?window:global);`;
|
|
134
|
-
}
|
|
30
|
+
// On supprime le try/catch. Si le fichier n'est pas trouvé, le processus plantera,
|
|
31
|
+
// ce qui est préférable à servir un code de secours potentiellement désynchronisé.
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
const __dirname = dirname(__filename);
|
|
34
|
+
const solverPath = join(__dirname, 'pow.solver.inline.js'); // Utilise la version inline
|
|
35
|
+
return readFileSync(solverPath, 'utf-8');
|
|
135
36
|
};
|
|
136
37
|
|
|
137
38
|
/**
|
|
@@ -190,7 +91,7 @@ function getHeaderSignature(context) {
|
|
|
190
91
|
for (let i = 0; i < context.rawHeaders.length; i += 2) {
|
|
191
92
|
headerKeys.push(context.rawHeaders[i]);
|
|
192
93
|
}
|
|
193
|
-
return cyrb53(headerKeys.join(','));
|
|
94
|
+
return cyrb53(headerKeys.sort().join(','));
|
|
194
95
|
}
|
|
195
96
|
|
|
196
97
|
/**
|
|
@@ -227,11 +128,6 @@ export function getCompositeDeviceHash(context) {
|
|
|
227
128
|
const ua = context.headers["user-agent"];
|
|
228
129
|
if (ua) {
|
|
229
130
|
srv.add("ua", ua);
|
|
230
|
-
// Extraire des infos supplémentaires du UA
|
|
231
|
-
const uaParts = parseUserAgent(ua);
|
|
232
|
-
if (uaParts.browser) srv.add("browser", uaParts.browser);
|
|
233
|
-
if (uaParts.os) srv.add("os_version", uaParts.os);
|
|
234
|
-
if (uaParts.device) srv.add("device_type", uaParts.device);
|
|
235
131
|
}
|
|
236
132
|
|
|
237
133
|
// 2. SIGNAL FORT: JA3 TLS Fingerprint
|
|
@@ -266,9 +162,6 @@ export function getCompositeDeviceHash(context) {
|
|
|
266
162
|
srv.add("upgrade", context.headers["upgrade-insecure-requests"]);
|
|
267
163
|
}
|
|
268
164
|
|
|
269
|
-
// 10. SIGNAL FORT: Ordonnancement des headers
|
|
270
|
-
srv.add("h_ord", getHeaderSignature(context));
|
|
271
|
-
|
|
272
165
|
// 11. SIGNAL AVANCÉ: Cookies (si disponible)
|
|
273
166
|
if (context.cookies) {
|
|
274
167
|
const cookieKeys = Object.keys(context.cookies).sort().join(',');
|
|
@@ -614,11 +507,11 @@ export const verifyPoWAndGenerateTicket = (
|
|
|
614
507
|
* Verifies a memory PoW solution.
|
|
615
508
|
* The server performs the same calculation to validate.
|
|
616
509
|
*/
|
|
617
|
-
export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret) => {
|
|
510
|
+
export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret = '') => {
|
|
618
511
|
const size = difficulty * 1024 * 1024;
|
|
619
512
|
const iterations = size / 16;
|
|
620
513
|
const buffer = new Uint32Array(size / 4);
|
|
621
|
-
const seed =
|
|
514
|
+
const seed = `:${nonce}:${clientSecret}`;
|
|
622
515
|
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
623
516
|
|
|
624
517
|
for (let i = 0; i < buffer.length; i++) {
|
|
@@ -946,8 +839,10 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
946
839
|
const now = Date.now();
|
|
947
840
|
const currentPath = context.path;
|
|
948
841
|
// Make the function robust to handle both URLSearchParams and plain objects for query.
|
|
949
|
-
|
|
950
|
-
|
|
842
|
+
const params =
|
|
843
|
+
context.query instanceof URLSearchParams
|
|
844
|
+
? new URLSearchParams(context.query.toString()) // Clone to avoid modifying the original
|
|
845
|
+
: new URLSearchParams(context.query || {});
|
|
951
846
|
params.sort(); // Sort for deterministic order
|
|
952
847
|
const currentQueryString = params.toString();
|
|
953
848
|
|
|
@@ -1365,20 +1260,44 @@ const BASE_TARGET = MAX_DIFFICULTY_TARGET >> 16n;
|
|
|
1365
1260
|
* @param {number} suspicionFactor - A number from 0 to 1.
|
|
1366
1261
|
* @returns {BigInt} The target number.
|
|
1367
1262
|
*/
|
|
1368
|
-
function calculateTarget(suspicionFactor) {
|
|
1263
|
+
function calculateTarget(suspicionFactor, securityConfig = {}) {
|
|
1369
1264
|
// Difficulty range adjusted to be realistic.
|
|
1370
1265
|
// MIN_DIFFICULTY: Fast enough not to bother a slightly suspicious user.
|
|
1371
1266
|
// MAX_DIFFICULTY: Slow enough to heavily penalize a bot, but feasible for a patient human (5-30s).
|
|
1372
|
-
|
|
1373
|
-
const
|
|
1267
|
+
// NOUVEAU: La difficulté est maintenant configurable.
|
|
1268
|
+
const { cpu: cpuConfig = {} } = securityConfig;
|
|
1269
|
+
const MIN_DIFFICULTY_BITS = cpuConfig.minDifficultyBits ?? 8;
|
|
1270
|
+
const MAX_DIFFICULTY_BITS = cpuConfig.maxDifficultyBits ?? 16;
|
|
1374
1271
|
|
|
1375
1272
|
// Use linear interpolation between min and max difficulty.
|
|
1376
1273
|
const totalDifficultyBits =
|
|
1377
1274
|
MIN_DIFFICULTY_BITS +
|
|
1378
1275
|
suspicionFactor * (MAX_DIFFICULTY_BITS - MIN_DIFFICULTY_BITS);
|
|
1276
|
+
|
|
1277
|
+
if (totalDifficultyBits <= 0) return 2n ** 256n - 1n; // Si la difficulté est nulle ou négative, la cible est maximale (aucun challenge).
|
|
1278
|
+
|
|
1279
|
+
// The correct way to calculate the target is to define the number of leading zero bits required.
|
|
1280
|
+
// A target for N bits of difficulty is 2^(256-N).
|
|
1281
|
+
// We can calculate this with a left-shift on 1.
|
|
1282
|
+
const shift = 256n - BigInt(Math.floor(totalDifficultyBits));
|
|
1283
|
+
return 1n << shift;
|
|
1284
|
+
}
|
|
1379
1285
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1286
|
+
/**
|
|
1287
|
+
* @private
|
|
1288
|
+
* Crée le bloc de base pour le challenge CPU.
|
|
1289
|
+
* Ce buffer contient toutes les données sauf la solution.
|
|
1290
|
+
* @param {string} nonce
|
|
1291
|
+
* @param {string} clientSecret
|
|
1292
|
+
* @param {string} fingerprint
|
|
1293
|
+
* @returns {Buffer}
|
|
1294
|
+
*/
|
|
1295
|
+
function createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint) {
|
|
1296
|
+
const sortedFingerprint = (fingerprint || '').split('|').filter(p => p).sort().join('|');
|
|
1297
|
+
// On concatène les chaînes, puis on les convertit en buffer une seule fois.
|
|
1298
|
+
// Cela garantit que le client et le serveur travaillent sur la même base binaire.
|
|
1299
|
+
const messageBase = `${nonce}:${clientSecret}:${sortedFingerprint}:`; // Le ':' final est le séparateur pour la solution.
|
|
1300
|
+
return Buffer.from(messageBase, 'utf8');
|
|
1382
1301
|
}
|
|
1383
1302
|
|
|
1384
1303
|
/**
|
|
@@ -1389,12 +1308,15 @@ export function generateCpuTargetChallenge(
|
|
|
1389
1308
|
nonce,
|
|
1390
1309
|
suspicionFactor,
|
|
1391
1310
|
originalUrl,
|
|
1311
|
+
securityConfig,
|
|
1392
1312
|
) {
|
|
1393
|
-
const target = calculateTarget(suspicionFactor);
|
|
1313
|
+
const target = calculateTarget(suspicionFactor, securityConfig);
|
|
1314
|
+
// Le baseBlock est créé ici et sera stocké dans le contexte du challenge.
|
|
1315
|
+
const baseBlock = createCpuChallengeBaseBlock(nonce, null, ''); // Pour le challenge simple, le secret et le fingerprint sont vides.
|
|
1394
1316
|
return {
|
|
1395
1317
|
type: "cpu_target",
|
|
1396
1318
|
nonce: nonce,
|
|
1397
|
-
target: target.toString(16),
|
|
1319
|
+
target: target.toString(16),
|
|
1398
1320
|
path: originalUrl,
|
|
1399
1321
|
};
|
|
1400
1322
|
}
|
|
@@ -1419,10 +1341,11 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
1419
1341
|
async function solve() {
|
|
1420
1342
|
const clientIp = ${JSON.stringify(clientIp)};
|
|
1421
1343
|
const nonce = ${JSON.stringify(nonce)};
|
|
1422
|
-
const cpuTarget = BigInt("0x${target}");
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1344
|
+
const cpuTarget = BigInt("0x" + "${target}");
|
|
1345
|
+
// La nouvelle version de solveCpuChallengeInline n'a plus besoin de l'IP ou du secret,
|
|
1346
|
+
// car tout est dans le baseBlock. Pour la compatibilité de ce challenge simple, on passe null.
|
|
1347
|
+
const baseBlockBytes = new TextEncoder().encode(nonce + ":");
|
|
1348
|
+
const solution = await window.solveCpuChallengeInline(baseBlockBytes, cpuTarget, (progress) => {});
|
|
1426
1349
|
window.location.href = ${JSON.stringify(path)} + "?pow_type=cpu_target&pow_nonce=" + nonce + "&pow_solution=" + solution;
|
|
1427
1350
|
}
|
|
1428
1351
|
solve();
|
|
@@ -1437,9 +1360,14 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
1437
1360
|
* @param {string} clientIp - The client's IP address.
|
|
1438
1361
|
* @returns {string} HTML content.
|
|
1439
1362
|
*/
|
|
1440
|
-
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml) {
|
|
1363
|
+
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml, originalFingerprint) {
|
|
1441
1364
|
const { nonce, target, path } = cpuChallengeDetails;
|
|
1442
1365
|
const solverCode = getPowSolverCode();
|
|
1366
|
+
// On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
|
|
1367
|
+
// Le fingerprint est maintenant passé directement en paramètre.
|
|
1368
|
+
const fingerprint = originalFingerprint;
|
|
1369
|
+
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint);
|
|
1370
|
+
const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
|
|
1443
1371
|
|
|
1444
1372
|
const challengeScript = `
|
|
1445
1373
|
async function solve() {
|
|
@@ -1447,22 +1375,18 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1447
1375
|
const path = ${JSON.stringify(path)};
|
|
1448
1376
|
const clientSecret = ${JSON.stringify(clientSecret)};
|
|
1449
1377
|
const clientIp = ${JSON.stringify(clientIp)};
|
|
1450
|
-
const cpuTarget = BigInt("0x${target}");
|
|
1378
|
+
const cpuTarget = BigInt("0x" + "${target}");
|
|
1451
1379
|
const memDifficulty = ${memoryDifficulty};
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
|
|
1455
|
-
const getClientFingerprint = () => (window.ClientLibrary && typeof window.ClientLibrary.getDeviceFingerprint === 'function') ? window.ClientLibrary.getDeviceFingerprint() : '';
|
|
1456
|
-
const fingerprint = getClientFingerprint();
|
|
1380
|
+
// Le client reçoit directement le 'baseBlock' sous forme de tableau d'octets.
|
|
1381
|
+
// Il n'a plus besoin de construire le message lui-même.
|
|
1382
|
+
const baseBlock = new Uint8Array(${baseBlockBytes});
|
|
1457
1383
|
|
|
1458
1384
|
// --- CPU Challenge ---
|
|
1459
|
-
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
|
|
1460
|
-
|
|
1461
|
-
// Optional progress callback
|
|
1462
|
-
});
|
|
1385
|
+
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...'; const cpuSolution = await window.solveCpuChallengeInline(baseBlock, cpuTarget, (progress) => {});
|
|
1386
|
+
|
|
1463
1387
|
// --- Memory Challenge ---
|
|
1464
1388
|
document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
|
|
1465
|
-
await new Promise(r => setTimeout(r, 10)); // Yield to update UI
|
|
1389
|
+
await new Promise(r => setTimeout(r, 10)); // Yield to update UI
|
|
1466
1390
|
let memSolution = 0;
|
|
1467
1391
|
try {
|
|
1468
1392
|
const memSeed = nonce + ":" + clientSecret;
|
|
@@ -1471,7 +1395,10 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1471
1395
|
document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
|
|
1472
1396
|
return;
|
|
1473
1397
|
}
|
|
1474
|
-
|
|
1398
|
+
|
|
1399
|
+
// Redirect with both solutions and the fingerprint used to solve.
|
|
1400
|
+
const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
1401
|
+
window.location.href = finalUrl;
|
|
1475
1402
|
}
|
|
1476
1403
|
solve();
|
|
1477
1404
|
`;
|
|
@@ -1502,23 +1429,56 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1502
1429
|
*/
|
|
1503
1430
|
export function verifyCpuTargetPoWAndGenerateTicket(
|
|
1504
1431
|
clientIp, // This parameter is crucial and must be the actual client IP
|
|
1505
|
-
ticketTtl,
|
|
1432
|
+
ticketTtl,
|
|
1506
1433
|
nonce,
|
|
1507
1434
|
solution,
|
|
1508
|
-
|
|
1509
|
-
target, // La cible est maintenant passée directement en hexadécimal,
|
|
1510
|
-
fingerprint, // Le fingerprint du SOLVER, soumis par le client
|
|
1435
|
+
challengeContext = {}, // Le contexte complet du challenge est maintenant passé
|
|
1511
1436
|
) {
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
1437
|
+
const { cpuTarget, baseBlock } = challengeContext;
|
|
1438
|
+
if (!cpuTarget || !baseBlock) {
|
|
1439
|
+
console.error('[FP Server Verify] Invalid challenge context. Missing cpuTarget or baseBlock.');
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Le baseBlock est déjà un Buffer ou un tableau d'octets.
|
|
1444
|
+
// On s'assure que c'est un Buffer pour la concaténation.
|
|
1445
|
+
const baseBlockBuffer = Buffer.isBuffer(baseBlock) ? baseBlock : Buffer.from(baseBlock);
|
|
1446
|
+
const solutionBuffer = Buffer.from(String(solution), 'utf8');
|
|
1447
|
+
|
|
1448
|
+
// Concaténation binaire directe. C'est la garantie de cohérence.
|
|
1449
|
+
const finalBlock = Buffer.concat([baseBlockBuffer, solutionBuffer]);
|
|
1450
|
+
|
|
1515
1451
|
const hash = crypto
|
|
1516
1452
|
.createHash("sha256")
|
|
1517
|
-
.update(
|
|
1453
|
+
.update(finalBlock)
|
|
1518
1454
|
.digest("hex");
|
|
1519
1455
|
const hashAsInt = BigInt("0x" + hash);
|
|
1456
|
+
const targetAsInt = BigInt("0x" + cpuTarget);
|
|
1457
|
+
|
|
1458
|
+
// --- NOUVEAUX LOGS POUR LE DÉBOGAGE ---
|
|
1459
|
+
console.log('[FP Server Verify] Intermediate values:', {
|
|
1460
|
+
hashCalculated: `0x${hash}`,
|
|
1461
|
+
hashAsInt: hashAsInt.toString(), // Log as string to see full value
|
|
1462
|
+
target: `0x${cpuTarget}`,
|
|
1463
|
+
targetAsInt: targetAsInt.toString(), // Log as string to see full value
|
|
1464
|
+
});
|
|
1465
|
+
// --- FIN DES NOUVEAUX LOGS ---
|
|
1466
|
+
|
|
1467
|
+
const isValid = hashAsInt < targetAsInt;
|
|
1520
1468
|
|
|
1521
|
-
|
|
1469
|
+
// --- AJOUT DE LOGS POUR LE DÉBOGAGE ---
|
|
1470
|
+
if (!isValid) {
|
|
1471
|
+
console.log('[FP Server Verify] CPU PoW verification FAILED. Details:', {
|
|
1472
|
+
hashCalculated: `0x${hash}`,
|
|
1473
|
+
target: `0x${cpuTarget}`,
|
|
1474
|
+
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
// --- FIN DES LOGS ---
|
|
1478
|
+
|
|
1479
|
+
if (isValid) {
|
|
1480
|
+
console.log('[FP Server Verify] CPU PoW verification PASSED. Details:', {
|
|
1481
|
+
});
|
|
1522
1482
|
// The comparison is direct with native BigInts
|
|
1523
1483
|
// The proof is valid, generate the ticket
|
|
1524
1484
|
const expiry = Date.now() + (ticketTtl || 3600000); // Calcule l'expiration à partir du TTL
|
|
@@ -1706,7 +1666,7 @@ export class FingerprintEngine {
|
|
|
1706
1666
|
if (deviceData?.condemned) {
|
|
1707
1667
|
this._log('Device condemned - blocking request', { deviceId });
|
|
1708
1668
|
if (onDeviceCompromised) {
|
|
1709
|
-
onDeviceCompromised({ deviceId:
|
|
1669
|
+
onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
|
|
1710
1670
|
}
|
|
1711
1671
|
return { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
1712
1672
|
}
|
|
@@ -1768,7 +1728,7 @@ export class FingerprintEngine {
|
|
|
1768
1728
|
// Si une solution de challenge est soumise, on la traite en priorité absolue,
|
|
1769
1729
|
// avant même de recalculer le score de suspicion.
|
|
1770
1730
|
const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem, pow_fp, pow_solution_population, pow_solution_work_result, pow_problem_id } = query;
|
|
1771
|
-
if (pow_nonce && (pow_solution ||
|
|
1731
|
+
if (pow_nonce && (pow_solution || pow_solution_cpu)) { // Vérifie pow_solution pour la compatibilité ascendante
|
|
1772
1732
|
this._log('Challenge solution submitted', { pow_type, pow_nonce });
|
|
1773
1733
|
|
|
1774
1734
|
// On doit calculer le score de suspicion *avant* de valider le ticket,
|
|
@@ -1795,14 +1755,33 @@ export class FingerprintEngine {
|
|
|
1795
1755
|
if (challengeContext) {
|
|
1796
1756
|
// *** NOUVELLE VÉRIFICATION CRUCIALE ***
|
|
1797
1757
|
// On compare le fingerprint soumis par le solver (`pow_fp`) avec celui stocké
|
|
1798
|
-
// lors de l'émission du challenge
|
|
1799
|
-
|
|
1800
|
-
|
|
1758
|
+
// lors de l'émission du challenge.
|
|
1759
|
+
// --- FIX: Use submitted fingerprint, but fallback to current request's fingerprint ---
|
|
1760
|
+
// This handles API clients that might not use the full client-side library but still solve the challenge.
|
|
1761
|
+
const solverFingerprint = pow_fp || getCompositeDeviceHash(requestContext);
|
|
1762
|
+
const originalFingerprint = challengeContext.fingerprint; // This is the fingerprint of the request that *triggered* the challenge
|
|
1763
|
+
|
|
1764
|
+
let similarity;
|
|
1765
|
+
const similarityThreshold = this.securityConfig.similarityThreshold ?? 0.95;
|
|
1766
|
+
|
|
1767
|
+
// If fingerprints are simple strings (like test placeholders 'fp-probation')
|
|
1768
|
+
// and don't contain the typical structure, fall back to a strict equality check.
|
|
1769
|
+
if (!originalFingerprint?.includes(':') || !solverFingerprint?.includes(':')) {
|
|
1770
|
+
similarity = (originalFingerprint === solverFingerprint) ? 1.0 : 0.0;
|
|
1771
|
+
} else {
|
|
1772
|
+
// Use the weighted comparison for structured fingerprints.
|
|
1773
|
+
// We compare the fingerprint of the request that triggered the challenge
|
|
1774
|
+
// with the fingerprint of the request that is submitting the solution.
|
|
1775
|
+
// They should be very similar.
|
|
1776
|
+
similarity = FingerprintBuilder.compare(originalFingerprint, getCompositeDeviceHash(requestContext));
|
|
1777
|
+
}
|
|
1801
1778
|
|
|
1802
|
-
if (
|
|
1779
|
+
if (similarity < similarityThreshold) {
|
|
1803
1780
|
this._log('Fingerprint mismatch - challenge solved on a different machine!', {
|
|
1804
1781
|
original: originalFingerprint,
|
|
1805
1782
|
solver: solverFingerprint,
|
|
1783
|
+
similarity: similarity.toFixed(4),
|
|
1784
|
+
threshold: similarityThreshold
|
|
1806
1785
|
});
|
|
1807
1786
|
isValid = false;
|
|
1808
1787
|
} else {
|
|
@@ -1810,13 +1789,13 @@ export class FingerprintEngine {
|
|
|
1810
1789
|
finalTtl = isProbationary ? probationaryTtl : optimalTtl;
|
|
1811
1790
|
this._log('Challenge context found, verifying solution', { optimalTtl, finalTtl });
|
|
1812
1791
|
|
|
1813
|
-
if (pow_type === "cpu_target" && pow_solution) {
|
|
1814
|
-
|
|
1792
|
+
if ((pow_type === "cpu_target" || !pow_type) && (pow_solution_cpu || pow_solution)) { // !pow_type pour compatibilité
|
|
1793
|
+
const cpuSolution = pow_solution_cpu || pow_solution;
|
|
1794
|
+
ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, cpuSolution, challengeContext);
|
|
1815
1795
|
isValid = ticket !== null;
|
|
1816
1796
|
this._log('CPU target challenge verification', { isValid });
|
|
1817
|
-
} else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) {
|
|
1818
|
-
|
|
1819
|
-
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret);
|
|
1797
|
+
} else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) { const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext);
|
|
1798
|
+
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret); // Memory PoW is independent of fingerprint
|
|
1820
1799
|
isValid = cpuTicket !== null && isMemValid;
|
|
1821
1800
|
if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
|
|
1822
1801
|
this._log('Combined CPU+Memory challenge verification', {
|
|
@@ -1840,7 +1819,7 @@ export class FingerprintEngine {
|
|
|
1840
1819
|
|
|
1841
1820
|
// NOUVELLE LOGIQUE DE REDIRECTION (plus robuste)
|
|
1842
1821
|
// 1. On part du chemin original stocké, qui peut contenir des query params.
|
|
1843
|
-
const originalUrl = new URL(challengeContext?.originalPath || path, `http://${requestContext.headers.host || 'localhost'}`);
|
|
1822
|
+
const originalUrl = new URL(challengeContext?.originalPath || requestContext.path, `http://${requestContext.headers.host || 'localhost'}`);
|
|
1844
1823
|
// 2. On crée un nouvel objet de paramètres à partir de la requête entrante (qui contient les solutions ET les params originaux).
|
|
1845
1824
|
const finalSearchParams = new URLSearchParams(requestContext.query);
|
|
1846
1825
|
|
|
@@ -1851,6 +1830,10 @@ export class FingerprintEngine {
|
|
|
1851
1830
|
finalSearchParams.delete('pow_solution_cpu');
|
|
1852
1831
|
finalSearchParams.delete('pow_solution_mem');
|
|
1853
1832
|
finalSearchParams.delete('pow_fp'); // Ne pas oublier de nettoyer le fingerprint
|
|
1833
|
+
// NOUVEAU: Nettoyer aussi les paramètres des challenges d'optimisation et de travail utile
|
|
1834
|
+
finalSearchParams.delete('pow_solution_population');
|
|
1835
|
+
finalSearchParams.delete('pow_solution_work_result');
|
|
1836
|
+
finalSearchParams.delete('pow_problem_id');
|
|
1854
1837
|
|
|
1855
1838
|
// 4. On reconstruit le chemin final.
|
|
1856
1839
|
const finalQueryString = finalSearchParams.toString();
|
|
@@ -1873,6 +1856,13 @@ export class FingerprintEngine {
|
|
|
1873
1856
|
this._log('Challenge solution invalid', { pow_nonce });
|
|
1874
1857
|
suspicionVector.honeypotScore = 100; // Invalid solution is a strong bot signal.
|
|
1875
1858
|
finalScore = this.calculateFinalScore(suspicionVector);
|
|
1859
|
+
// --- FIX: After invalidating a solution, immediately check if the new score triggers a block ---
|
|
1860
|
+
const newBlockThreshold = thresholds.block ?? 95;
|
|
1861
|
+
if (finalScore >= newBlockThreshold) {
|
|
1862
|
+
this._log('Request blocked after invalid challenge solution', { finalScore, newBlockThreshold });
|
|
1863
|
+
return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
|
|
1864
|
+
}
|
|
1865
|
+
// If not blocked, the request will proceed to be re-challenged.
|
|
1876
1866
|
}
|
|
1877
1867
|
} else if (pow_nonce && pow_type === 'optimization_task' && pow_solution_population) {
|
|
1878
1868
|
this._log('Optimization task solution submitted', { pow_nonce });
|
|
@@ -1935,7 +1925,7 @@ export class FingerprintEngine {
|
|
|
1935
1925
|
if (isBlocked) {
|
|
1936
1926
|
this._log('Request blocked - score exceeded block threshold', { finalScore, blockThreshold });
|
|
1937
1927
|
if (onDeviceCompromised) {
|
|
1938
|
-
onDeviceCompromised({ deviceId:
|
|
1928
|
+
onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
|
|
1939
1929
|
}
|
|
1940
1930
|
return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
|
|
1941
1931
|
}
|
|
@@ -1947,7 +1937,7 @@ export class FingerprintEngine {
|
|
|
1947
1937
|
this._log('Honeypot trap URL triggered - condemning device', { path, deviceId });
|
|
1948
1938
|
deviceData.condemned = true; // This device is a bot. Condemn it.
|
|
1949
1939
|
if (onDeviceCompromised) {
|
|
1950
|
-
onDeviceCompromised({ deviceId:
|
|
1940
|
+
onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
|
|
1951
1941
|
}
|
|
1952
1942
|
if (logger) {
|
|
1953
1943
|
logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now() });
|
|
@@ -2002,8 +1992,8 @@ export class FingerprintEngine {
|
|
|
2002
1992
|
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
|
|
2003
1993
|
const trapLinksHtml = trapUrls.map(url => `<a href="${url}" tabindex="-1">config</a>`).join(' ');
|
|
2004
1994
|
|
|
2005
|
-
|
|
2006
|
-
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
|
|
1995
|
+
// On passe la configuration pour que la difficulté soit calculée correctement.
|
|
1996
|
+
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
2007
1997
|
|
|
2008
1998
|
// La difficulté mémoire démarre à 0 et augmente seulement après un certain seuil de suspicion.
|
|
2009
1999
|
// Par exemple, elle ne commence à augmenter qu'à partir de 25% du chemin entre 'low' et 'high'.
|
|
@@ -2019,16 +2009,18 @@ export class FingerprintEngine {
|
|
|
2019
2009
|
memDifficulty,
|
|
2020
2010
|
cpuTarget: cpuChallengeDetails.target
|
|
2021
2011
|
});
|
|
2022
|
-
// (NOUVEAU) On stocke le fingerprint de la requête qui a déclenché le challenge.
|
|
2023
|
-
const originalFingerprint = requestContext.headers['x-device-fingerprint'] || getCompositeDeviceHash(requestContext);
|
|
2012
|
+
// (NOUVEAU) On stocke le fingerprint de la requête qui a déclenché le challenge. We call it via __internal to allow mocking.
|
|
2013
|
+
const originalFingerprint = requestContext.headers['x-device-fingerprint'] || __internal.getCompositeDeviceHash(requestContext);
|
|
2024
2014
|
|
|
2025
2015
|
// Store the entire challenge context with a short TTL (e.g., 5 minutes)
|
|
2016
|
+
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, originalFingerprint);
|
|
2026
2017
|
await store.set(`secret:${nonce}`, {
|
|
2027
2018
|
clientSecret,
|
|
2028
2019
|
cpuTarget: cpuChallengeDetails.target,
|
|
2029
2020
|
suspicionScore: finalScore, // *** FIX: Store the score that triggered the challenge ***
|
|
2030
2021
|
fingerprint: originalFingerprint, // *** NOUVEAU ***
|
|
2031
2022
|
memDifficulty: memDifficulty,
|
|
2023
|
+
baseBlock: baseBlock, // *** NOUVEAU: Le bloc de base est stocké pour la vérification ***
|
|
2032
2024
|
originalPath: path, // *** FIX: Store the original path ***
|
|
2033
2025
|
}, this.securityConfig.challengeTtl || 300); // NOUVEAU: TTL configurable (5min par défaut)
|
|
2034
2026
|
|
|
@@ -2060,13 +2052,14 @@ export class FingerprintEngine {
|
|
|
2060
2052
|
clientSecret: clientSecret, // The client needs this to solve the challenge
|
|
2061
2053
|
cpuTarget: cpuChallengeDetails.target,
|
|
2062
2054
|
memDifficulty: memDifficulty,
|
|
2055
|
+
baseBlock: [...baseBlock], // Envoyer le buffer comme un tableau d'octets
|
|
2063
2056
|
}
|
|
2064
2057
|
};
|
|
2065
2058
|
this._log('API challenge response generated', { challengePayload });
|
|
2066
2059
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
|
|
2067
2060
|
} else {
|
|
2068
2061
|
// For browsers, send the HTML page.
|
|
2069
|
-
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);
|
|
2062
|
+
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, originalFingerprint);
|
|
2070
2063
|
this._log('Browser challenge page generated', {
|
|
2071
2064
|
pageLength: page.length,
|
|
2072
2065
|
hasTrapContainer: true
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file @/optimization.worker.js
|
|
3
|
+
* @description Web Worker générique pour exécuter les algorithmes d'optimisation de la bibliothèque `Optimization`.
|
|
4
|
+
* Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
8
|
+
import { Optimization } from './library.js'; // Assurez-vous que le chemin est correct
|
|
9
|
+
|
|
10
|
+
if (parentPort) {
|
|
11
|
+
parentPort.on('message', async () => { // Le message est vide, on utilise workerData
|
|
12
|
+
const { solverName, solverArgs } = workerData;
|
|
13
|
+
|
|
14
|
+
// Gérer les solveurs imbriqués (ex: 'Operators.solvePortfolio')
|
|
15
|
+
const solverFunction = solverName.split('.').reduce((obj, prop) => obj && obj[prop], Optimization);
|
|
16
|
+
|
|
17
|
+
if (typeof solverFunction === 'function') {
|
|
18
|
+
try {
|
|
19
|
+
const result = await solverFunction(...solverArgs);
|
|
20
|
+
parentPort.postMessage(result);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
parentPort.postMessage({ error: error.message, stack: error.stack });
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
parentPort.postMessage({ error: `Solver '${solverName}' not found or is not a function in Optimization library.` });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
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
5
|
"main": "fingerprint.js",
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"fingerprint.client.js",
|
|
16
16
|
"fingerprint.builder.js",
|
|
17
17
|
"pow.solver.js",
|
|
18
|
+
"pow.worker.js",
|
|
19
|
+
"problem-manager.js",
|
|
20
|
+
"optimization.worker.js",
|
|
18
21
|
"library.js",
|
|
19
22
|
"redis-store.js",
|
|
20
23
|
"mongodb-store.js",
|
package/pow.solver.js
CHANGED
|
@@ -10,26 +10,42 @@
|
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Résout un challenge CPU basé sur une cible
|
|
14
|
-
* @param {
|
|
15
|
-
* @param {string} nonce - Le nonce du challenge.
|
|
13
|
+
* Résout un challenge CPU basé sur une cible en utilisant un bloc de base binaire.
|
|
14
|
+
* @param {Uint8Array} baseBlock - Le bloc de données initial (nonce, secret, fp) fourni par le serveur.
|
|
16
15
|
* @param {bigint} target - La cible à atteindre.
|
|
17
|
-
* @param {string} clientSecret - Le secret client (optionnel).
|
|
18
16
|
* @param {Function} progressCallback - Callback pour les mises à jour de progression.
|
|
19
17
|
* @returns {Promise<number>} La solution (un nombre entier).
|
|
20
18
|
*/
|
|
21
|
-
export async function solveCpuTargetInline(
|
|
22
|
-
//
|
|
23
|
-
|
|
19
|
+
export async function solveCpuTargetInline(baseBlock, target, progressCallback) {
|
|
20
|
+
// --- FIX: Add validation for the target to prevent BigInt conversion errors ---
|
|
21
|
+
if (typeof target !== 'bigint' && (typeof target !== 'string' || !/^[0-9a-fA-F]+$/.test(target))) {
|
|
22
|
+
throw new TypeError(`Invalid target type: expected a BigInt or a hex string, but got ${typeof target} with value ${target}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
24
25
|
const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
|
|
26
|
+
// --- END FIX ---
|
|
27
|
+
const encoder = new TextEncoder();
|
|
25
28
|
let cpuSolution = 0;
|
|
26
|
-
|
|
27
|
-
while (true) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
29
|
+
|
|
30
|
+
while (true) {
|
|
31
|
+
const solutionBytes = encoder.encode(String(cpuSolution));
|
|
32
|
+
|
|
33
|
+
// Concaténation binaire directe : c'est plus rapide et plus sûr.
|
|
34
|
+
const finalBlock = new Uint8Array(baseBlock.length + solutionBytes.length);
|
|
35
|
+
finalBlock.set(baseBlock);
|
|
36
|
+
finalBlock.set(solutionBytes, baseBlock.length);
|
|
37
|
+
|
|
38
|
+
if (cpuSolution === 0) {
|
|
39
|
+
const reconstructedMsg = new TextDecoder().decode(finalBlock);
|
|
40
|
+
console.log('[FP Solve Debug] Client will hash message:', reconstructedMsg);
|
|
41
|
+
}
|
|
42
|
+
const buf = await crypto.subtle.digest("SHA-256", finalBlock);
|
|
32
43
|
const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
44
|
+
// --- AJOUT DE LOGS POUR LE DÉBOGAGE CÔTÉ CLIENT ---
|
|
45
|
+
if (cpuSolution === 0) { // Log only the first attempt
|
|
46
|
+
console.log(`[FP Client Solve] Attempt 0 hash: "0x${hashHex}"`);
|
|
47
|
+
}
|
|
48
|
+
// --- FIN DES LOGS ---
|
|
33
49
|
if (BigInt('0x' + hashHex) < cpuTarget) break;
|
|
34
50
|
cpuSolution++;
|
|
35
51
|
if (cpuSolution % 100000 === 0) {
|
|
@@ -328,27 +344,30 @@ export async function solveOptimizationTask(initialPopulation, generations) {
|
|
|
328
344
|
* @param {object} challenge - L'objet challenge reçu du serveur.
|
|
329
345
|
* @returns {Promise<object>} Un objet contenant la ou les solutions.
|
|
330
346
|
*/
|
|
331
|
-
export async function solveChallenge(challenge) {
|
|
347
|
+
export async function solveChallenge(challenge, fingerprint = '') { // The fingerprint is now passed from the client library
|
|
332
348
|
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance, optimizationTask, usefulWorkTask } = challenge;
|
|
333
349
|
const solutions = {};
|
|
334
350
|
|
|
335
351
|
switch (type) {
|
|
336
352
|
case 'cpu_target':
|
|
337
|
-
|
|
353
|
+
// Note: This case is not fully exercised by tests as it relies on Web Workers.
|
|
338
354
|
if (!cpuTarget) {
|
|
339
355
|
throw new Error("Challenge data is missing 'cpuTarget' property.");
|
|
340
356
|
}
|
|
341
357
|
const target = cpuTarget; // Keep variable name for consistency below
|
|
342
|
-
|
|
358
|
+
// Pour ce challenge simple, le baseBlock est juste le nonce.
|
|
359
|
+
const baseBlockBytes = new TextEncoder().encode(nonce + ":");
|
|
360
|
+
solutions.cpu = await solveCpuTargetInline(baseBlockBytes, target, null);
|
|
343
361
|
break;
|
|
344
362
|
case 'cpu_mem':
|
|
345
363
|
// Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
|
|
346
|
-
const baseMessageCombined =
|
|
364
|
+
const baseMessageCombined = `:${nonce}:${clientSecret}`;
|
|
347
365
|
const memSeed = `:${nonce}:${clientSecret}`;
|
|
348
366
|
const [cpuSol, memSol] = await Promise.all([
|
|
349
367
|
(async () => {
|
|
350
|
-
if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property.");
|
|
351
|
-
|
|
368
|
+
if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property."); // Pass fingerprint to solver
|
|
369
|
+
const baseBlock = new Uint8Array(challenge.baseBlock);
|
|
370
|
+
return solveCpuTargetInline(baseBlock, cpuTarget, null);
|
|
352
371
|
})(),
|
|
353
372
|
solveMemory(memSeed, memDifficulty)
|
|
354
373
|
]);
|
|
@@ -357,11 +376,12 @@ export async function solveChallenge(challenge) {
|
|
|
357
376
|
break;
|
|
358
377
|
case 'cpu_mem_inline':
|
|
359
378
|
// Version inline pour compatibilité HTML avec IP incluse
|
|
360
|
-
const memSeedInline =
|
|
379
|
+
const memSeedInline = `:${nonce}:${clientSecret}`;
|
|
361
380
|
const [cpuSolInline, memSolInline] = await Promise.all([
|
|
362
381
|
(async () => {
|
|
363
382
|
if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property.");
|
|
364
|
-
|
|
383
|
+
const baseBlock = new Uint8Array(challenge.baseBlock);
|
|
384
|
+
return solveCpuTargetInline(baseBlock, cpuTarget, null);
|
|
365
385
|
})(),
|
|
366
386
|
solveMemory(memSeedInline, memDifficulty)
|
|
367
387
|
]);
|
package/pow.worker.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file @/pow.worker.js
|
|
3
|
+
* @description Web Worker dédié à la résolution du challenge CPU.
|
|
4
|
+
* Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
self.onmessage = async (event) => {
|
|
8
|
+
const { message, target } = event.data;
|
|
9
|
+
let solution = 0;
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
|
|
12
|
+
// La boucle de calcul intensive est isolée dans ce worker.
|
|
13
|
+
while (true) {
|
|
14
|
+
const currentMessage = `${message}:${solution}`;
|
|
15
|
+
const data = encoder.encode(currentMessage);
|
|
16
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
17
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
18
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
19
|
+
|
|
20
|
+
if (BigInt('0x' + hashHex) < target) {
|
|
21
|
+
// Une fois la solution trouvée, on la renvoie au thread principal.
|
|
22
|
+
self.postMessage({ solution });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
solution++;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { Optimization } from './library.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @namespace ProblemInitializers
|
|
6
|
+
* @description Fonctions pour générer dynamiquement les données d'un problème.
|
|
7
|
+
*/
|
|
8
|
+
const ProblemInitializers = {
|
|
9
|
+
/**
|
|
10
|
+
* Génère un ensemble de points aléatoires pour un problème de TSP.
|
|
11
|
+
* @param {object} params - Les paramètres de génération.
|
|
12
|
+
* @param {number} params.count - Le nombre de points à générer.
|
|
13
|
+
* @param {{x: number, y: number}} [params.bounds={x: 1000, y: 1000}] - Les limites spatiales.
|
|
14
|
+
* @returns {Array<{x: number, y: number}>}
|
|
15
|
+
*/
|
|
16
|
+
'generate:randomPoints': (params) => {
|
|
17
|
+
const { count, bounds = { x: 1000, y: 1000 } } = params;
|
|
18
|
+
if (isNaN(count)) return [];
|
|
19
|
+
return Array.from({ length: count }, () => ({ x: Math.random() * bounds.x, y: Math.random() * bounds.y }));
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Génère un ensemble d'actifs financiers aléatoires pour un problème de portefeuille.
|
|
24
|
+
* @param {object} params - Les paramètres de génération.
|
|
25
|
+
* @param {number} params.count - Le nombre d'actifs à générer.
|
|
26
|
+
* @returns {Array<{expectedReturn: number, volatility: number}>}
|
|
27
|
+
*/
|
|
28
|
+
'generate:randomAssets': (params) => {
|
|
29
|
+
const { count } = params;
|
|
30
|
+
if (isNaN(count)) return [];
|
|
31
|
+
return Array.from({ length: count }, () => ({
|
|
32
|
+
expectedReturn: Math.random() * 0.2,
|
|
33
|
+
volatility: 0.1 + Math.random() * 0.3
|
|
34
|
+
}));
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Crée une fonction qui génère des arguments pour chaque worker de `runMultipleParallel`.
|
|
39
|
+
* Permet de faire varier les paramètres (ex: solution initiale) pour chaque cycle.
|
|
40
|
+
* @param {object} params - Les paramètres de configuration.
|
|
41
|
+
* @param {Array<any>} params.baseArgs - Les arguments de base, communs à tous les workers.
|
|
42
|
+
* @param {object} params.variations - Décrit comment faire varier un argument.
|
|
43
|
+
* @returns {function(number): Array<any>} La fonction `workerDataGenerator`.
|
|
44
|
+
*/
|
|
45
|
+
'generate:parallelArgs': (params) => {
|
|
46
|
+
const { baseArgs, variations } = params;
|
|
47
|
+
return (cycleIndex) => {
|
|
48
|
+
const cycleArgs = [...baseArgs];
|
|
49
|
+
// Pour l'instant, on gère la variation de la solution initiale pour le TSP
|
|
50
|
+
if (variations?.initialSolution === 'random') {
|
|
51
|
+
cycleArgs[0] = cycleArgs[0].sort(() => Math.random() - 0.5);
|
|
52
|
+
}
|
|
53
|
+
return cycleArgs;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
class ProblemManager {
|
|
59
|
+
constructor(configPath) {
|
|
60
|
+
this.configPath = configPath;
|
|
61
|
+
this.problems = this.loadProblems();
|
|
62
|
+
this.currentProblemIndex = 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
loadProblems() {
|
|
66
|
+
try {
|
|
67
|
+
const data = readFileSync(this.configPath, 'utf-8');
|
|
68
|
+
const problems = JSON.parse(data);
|
|
69
|
+
// Initialisation dynamique des problèmes
|
|
70
|
+
for (const problem of problems) { // eslint-disable-line no-unused-vars
|
|
71
|
+
for (const key in problem.payload) {
|
|
72
|
+
const value = problem.payload[key];
|
|
73
|
+
// On cherche une instruction d'initialisation (ex: { "$init": "generate:randomPoints", ... })
|
|
74
|
+
if (typeof value === 'object' && value !== null && value.$init) {
|
|
75
|
+
const initializer = ProblemInitializers[value.$init];
|
|
76
|
+
if (initializer) {
|
|
77
|
+
// On remplace l'objet d'instruction par les données générées.
|
|
78
|
+
problem.payload[key] = initializer(value.params || {});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return problems;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`[ProblemManager] Erreur lors du chargement du fichier de problèmes: ${error.message}`);
|
|
86
|
+
return []; // Retourne un tableau vide en cas d'erreur pour éviter un crash
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
saveProblems() {
|
|
91
|
+
// Note: Dans un vrai scénario, utilisez une base de données pour éviter les race conditions.
|
|
92
|
+
try {
|
|
93
|
+
writeFileSync(this.configPath, JSON.stringify(this.problems, null, 2));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`[ProblemManager] Erreur lors de la sauvegarde du fichier de problèmes: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sélectionne un problème et génère une unité de travail.
|
|
101
|
+
* @param {number} suspicionFactor - Le facteur de suspicion pour ajuster la difficulté.
|
|
102
|
+
* @returns {{problemId: string, task: object}|null}
|
|
103
|
+
*/
|
|
104
|
+
dispatchWork(suspicionFactor) {
|
|
105
|
+
if (this.problems.length === 0) return null;
|
|
106
|
+
|
|
107
|
+
const problem = this.problems[this.currentProblemIndex];
|
|
108
|
+
this.currentProblemIndex = (this.currentProblemIndex + 1) % this.problems.length;
|
|
109
|
+
|
|
110
|
+
const task = { type: problem.workUnit.type };
|
|
111
|
+
const { scalingFactor } = problem.workUnit;
|
|
112
|
+
|
|
113
|
+
switch (problem.workUnit.type) {
|
|
114
|
+
case 'simulated_annealing_iterations':
|
|
115
|
+
// Assurer une difficulté minimale pour que le challenge soit significatif
|
|
116
|
+
const baseIterations = Math.max(15000, problem.workUnit.baseIterations || 0);
|
|
117
|
+
task.iterations = scalingFactor
|
|
118
|
+
? Math.floor(baseIterations * Math.pow(scalingFactor, suspicionFactor))
|
|
119
|
+
: Math.floor(baseIterations * (0.5 + suspicionFactor));
|
|
120
|
+
task.payload = problem.payload;
|
|
121
|
+
task.initialSolution = problem.state.bestSolution;
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case 'genetic_algorithm_generations':
|
|
125
|
+
// Assurer une difficulté minimale pour que le challenge soit significatif
|
|
126
|
+
const baseGenerations = Math.max(50, problem.workUnit.baseGenerations || 0);
|
|
127
|
+
task.generations = scalingFactor
|
|
128
|
+
? Math.floor(baseGenerations * Math.pow(scalingFactor, suspicionFactor))
|
|
129
|
+
: Math.floor(baseGenerations * (0.5 + suspicionFactor));
|
|
130
|
+
task.payload = problem.payload;
|
|
131
|
+
task.initialPopulation = problem.state.population;
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case 'run_multiple_parallel':
|
|
135
|
+
task.solverName = problem.workUnit.solverName;
|
|
136
|
+
task.numCycles = problem.workUnit.numCycles;
|
|
137
|
+
// Les arguments et le générateur sont dans le payload pour plus de flexibilité
|
|
138
|
+
task.baseSolverArgs = problem.payload.baseSolverArgs;
|
|
139
|
+
task.workerDataGenerator = problem.payload.workerDataGenerator;
|
|
140
|
+
task.logProgress = problem.payload.logProgress || false;
|
|
141
|
+
task.concurrency = problem.payload.concurrency;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { problemId: problem.id, task };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Intègre la solution d'un client dans l'état du problème.
|
|
150
|
+
* @param {string} problemId - L'ID du problème.
|
|
151
|
+
* @param {object} solutionData - La solution renvoyée par le client.
|
|
152
|
+
*/
|
|
153
|
+
integrateSolution(problemId, solutionData) {
|
|
154
|
+
const problem = this.problems.find(p => p.id === problemId);
|
|
155
|
+
if (!problem) return;
|
|
156
|
+
|
|
157
|
+
switch (problem.workUnit.type) {
|
|
158
|
+
case 'simulated_annealing_iterations':
|
|
159
|
+
if (solutionData.energy < (parseFloat(problem.state.bestEnergy) || Infinity)) {
|
|
160
|
+
problem.state.bestSolution = solutionData.solution;
|
|
161
|
+
problem.state.bestEnergy = solutionData.energy;
|
|
162
|
+
console.log(`[ProblemManager] Nouvelle meilleure solution pour ${problemId}: ${solutionData.energy.toFixed(2)}`);
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
case 'genetic_algorithm_generations':
|
|
166
|
+
problem.state.population = solutionData.population;
|
|
167
|
+
console.log(`[ProblemManager] Population mise à jour pour ${problemId}.`);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
this.saveProblems();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { ProblemManager }; // Export the class for testing
|
|
175
|
+
export const problemManager = new ProblemManager('./problems.config.json');
|