@anonympins/fingerprint 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -10
- package/fingerprint.client.js +51 -2
- package/fingerprint.js +267 -132
- package/library.js +1577 -1446
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ The process unfolds in three steps:
|
|
|
25
25
|
* **Low to Medium Suspicion**: A combined CPU and Memory Proof-of-Work (PoW) challenge is issued. The difficulty of both the CPU (hash calculation) and Memory (allocation and computation) components scales progressively with the suspicion score. For low scores, the memory challenge is negligible, making it primarily a CPU task.
|
|
26
26
|
* **High Suspicion**: For the most suspicious requests, the system issues a high-difficulty combined CPU/Memory challenge. The architecture allows for plugging in more complex challenges like CAPTCHAs if needed.
|
|
27
27
|
|
|
28
|
-
Once the challenge is solved, a clearance "ticket" is issued via a secure cookie, exempting the user from new challenges for a set period.
|
|
28
|
+
Once the challenge is solved, a clearance "ticket" is issued via a secure cookie, exempting the user from new challenges for a set period. For API clients, the challenge is delivered as a `429` JSON response, and the client is expected to solve it and retry the request.
|
|
29
29
|
|
|
30
30
|
## Features
|
|
31
31
|
|
|
@@ -81,21 +81,33 @@ const securityConfig = {
|
|
|
81
81
|
headerAnomalyScore: 0.1, // Penalizes abnormal headers (missing UA, etc.)
|
|
82
82
|
requestPatternScore: 0.6,// Penalizes bot-like request sequences (scraping, etc.)
|
|
83
83
|
inconsistencyScore: 0.8, // Strongly penalizes inconsistency between the current and initial fingerprint (stolen cookie)
|
|
84
|
+
behaviorScore: 0.7, // Penalizes non-human interactions (no mouse/keyboard activity)
|
|
84
85
|
honeypotScore: 1.0 // Strongly penalizes bots filling hidden form fields
|
|
85
86
|
},
|
|
87
|
+
// A new, non-suspicious device will always have its score adjusted to a minimum of 1, ensuring it receives a minimal, almost imperceptible challenge on its first visit.
|
|
86
88
|
thresholds: {
|
|
87
89
|
low: 20, // Score from which a CPU challenge is issued
|
|
88
90
|
medium: 45, // Score for a more difficult combined CPU/Memory challenge
|
|
89
91
|
high: 75, // Score for a very difficult challenge
|
|
90
92
|
block: 95, // Score above which the request is blocked outright (HTTP 403)
|
|
91
|
-
isStaticResource: (req) => req.path.startsWith('/static/') // Optional: Custom function to identify static resources
|
|
93
|
+
isStaticResource: (req) => req.path.startsWith('/static/'), // Optional: Custom function to identify static resources
|
|
94
|
+
isApiRequest: (req) => req.path.startsWith('/api/') || req.headers.accept?.includes('application/json') // Optional: Custom function to identify API requests
|
|
92
95
|
},
|
|
96
|
+
// (Optional) Configure the duration (in milliseconds) for various temporary data.
|
|
97
|
+
ticketMaxAge: 3600000, // 1 hour. Duration for which a solved challenge ticket is valid.
|
|
98
|
+
challengeTtl: 300000, // 5 minutes. Time during which a challenge nonce is valid.
|
|
99
|
+
deviceIdCookieMaxAge: undefined, // By default, it's a session cookie. Set a value in ms for a persistent cookie.
|
|
100
|
+
|
|
93
101
|
patterns: { // (Optional) Initial values for request pattern detection, optimized by auto-tuner if enabled.
|
|
94
|
-
velocityThreshold: 200,
|
|
95
|
-
burstThreshold: 500,
|
|
96
|
-
scrapeThreshold: 1000,
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
velocityThreshold: 200, // ms between requests to be considered "fast"
|
|
103
|
+
burstThreshold: 500, // ms for identical requests to be a "burst"
|
|
104
|
+
scrapeThreshold: 1000, // ms for sequential requests to be "scraping"
|
|
105
|
+
scrapeBurstWeight: 40, // Additional weight for repeated scraping patterns
|
|
106
|
+
sequenceLength: 3, // Length of a request sequence to detect (e.g., A->B->C)
|
|
107
|
+
sequenceWeight: 60, // Penalty for repeating a sequence
|
|
108
|
+
historySize: 10, // Number of requests to keep for pattern analysis
|
|
109
|
+
decayFactor: 0.9, // How quickly the pattern score decays over time
|
|
110
|
+
inactivityReset: 30000, // ms of inactivity after which the pattern score is reset
|
|
99
111
|
},
|
|
100
112
|
honeypot: {
|
|
101
113
|
// List of field names that are traps for bots.
|
|
@@ -344,9 +356,13 @@ initializeClient({
|
|
|
344
356
|
// (Optional) Enables automatic protection for `fetch` requests.
|
|
345
357
|
// If the `fetch` object is present, the protection is active.
|
|
346
358
|
fetch: {
|
|
347
|
-
// (Optional) An array of domains to protect. If empty or not provided,
|
|
348
|
-
|
|
349
|
-
|
|
359
|
+
// (Optional) An array of domains to protect. If empty or not provided, it protects same-origin requests by default.
|
|
360
|
+
targetDomains: ['api.yourdomain.com', 'auth.yourdomain.com'],
|
|
361
|
+
|
|
362
|
+
// (Optional, default: true) If enabled, the client will automatically intercept 429 challenge responses,
|
|
363
|
+
// solve the PoW in the background, and retry the original request with the solution.
|
|
364
|
+
// This makes the protection seamless for API clients that use this library.
|
|
365
|
+
handleChallenges: true
|
|
350
366
|
}
|
|
351
367
|
});
|
|
352
368
|
```
|
package/fingerprint.client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cyrb53, FingerprintBuilder } from './fingerprint.builder.js';
|
|
2
|
+
import { solveChallenge } from './pow.solver.js';
|
|
2
3
|
|
|
3
4
|
const ClientLibrary = {
|
|
4
5
|
// Cache pour éviter de recalculer les constantes (Hardware, etc.)
|
|
@@ -320,6 +321,44 @@ const ClientLibrary = {
|
|
|
320
321
|
this.addFetchInterceptor(fingerprintInterceptor);
|
|
321
322
|
},
|
|
322
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Intercepte une réponse de challenge JSON, le résout, et réessaie la requête.
|
|
326
|
+
* @param {Response} response - La réponse initiale (potentiellement 429).
|
|
327
|
+
* @param {RequestInfo} resource - La ressource de la requête originale.
|
|
328
|
+
* @param {RequestInit} options - Les options de la requête originale.
|
|
329
|
+
* @returns {Promise<Response>} - La réponse de la requête réessayée.
|
|
330
|
+
* @private
|
|
331
|
+
*/
|
|
332
|
+
async solveChallengeAndRetry(response, resource, options) {
|
|
333
|
+
if (response.status !== 429) {
|
|
334
|
+
return response;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const challengeData = await response.json();
|
|
339
|
+
if (!challengeData.challenge || !challengeData.challenge.type) {
|
|
340
|
+
return response; // Pas un challenge JSON valide
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
|
|
344
|
+
const solution = await solveChallenge(challengeData.challenge);
|
|
345
|
+
console.log('[Fingerprint] Challenge solved. Retrying original request.');
|
|
346
|
+
|
|
347
|
+
// Ajouter la solution aux paramètres de la requête pour le nouvel essai
|
|
348
|
+
const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
|
|
349
|
+
url.searchParams.set('pow_type', challengeData.challenge.type);
|
|
350
|
+
url.searchParams.set('pow_nonce', challengeData.challenge.nonce);
|
|
351
|
+
// La solution peut être un objet (pour les challenges combinés) ou une valeur simple
|
|
352
|
+
Object.entries(solution).forEach(([key, value]) => {
|
|
353
|
+
url.searchParams.set(`pow_solution_${key}`, value);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return this._originalFetch(url.toString(), options);
|
|
357
|
+
} catch (e) {
|
|
358
|
+
console.error('[Fingerprint] Failed to solve or retry challenge:', e);
|
|
359
|
+
return response; // Retourne la réponse 429 originale en cas d'échec
|
|
360
|
+
}
|
|
361
|
+
},
|
|
323
362
|
/**
|
|
324
363
|
* @typedef {object} ClientConfig
|
|
325
364
|
* @property {boolean} [mouse=true] - Activer le suivi de l'entropie de la souris.
|
|
@@ -339,7 +378,7 @@ const ClientLibrary = {
|
|
|
339
378
|
mouse = true,
|
|
340
379
|
keystrokes = true,
|
|
341
380
|
honeypots = [],
|
|
342
|
-
fetch: fetchConfig,
|
|
381
|
+
fetch: fetchConfig = {},
|
|
343
382
|
} = config;
|
|
344
383
|
|
|
345
384
|
if (mouse) {
|
|
@@ -351,8 +390,17 @@ const ClientLibrary = {
|
|
|
351
390
|
if (honeypots.length > 0) {
|
|
352
391
|
this.initializeHoneypots(honeypots);
|
|
353
392
|
}
|
|
354
|
-
|
|
393
|
+
// On vérifie si l'objet fetch est présent pour activer l'interception
|
|
394
|
+
if (Object.keys(fetchConfig).length > 0 || config.fetch) {
|
|
355
395
|
this.initializeFetch(fetchConfig.targetDomains);
|
|
396
|
+
|
|
397
|
+
// Ajoute l'intercepteur pour la résolution de challenge
|
|
398
|
+
if (fetchConfig.handleChallenges !== false) {
|
|
399
|
+
this.addFetchInterceptor(async (resource, options, next) => {
|
|
400
|
+
const response = await next(resource, options);
|
|
401
|
+
return this.solveChallengeAndRetry(response, resource, options);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
356
404
|
}
|
|
357
405
|
}
|
|
358
406
|
};
|
|
@@ -394,6 +442,7 @@ export const addFetchInterceptor = ClientLibrary.addFetchInterceptor.bind(Client
|
|
|
394
442
|
export const patchGlobalFetch = ClientLibrary.patchGlobalFetch.bind(ClientLibrary);
|
|
395
443
|
export const initializeFetch = ClientLibrary.initializeFetch.bind(ClientLibrary);
|
|
396
444
|
export const initializeClient = ClientLibrary.initializeClient.bind(ClientLibrary);
|
|
445
|
+
export const solveChallengeAndRetry = ClientLibrary.solveChallengeAndRetry.bind(ClientLibrary);
|
|
397
446
|
|
|
398
447
|
// Export the internal object for testing purposes
|
|
399
448
|
export default ClientLibrary;
|