@anonympins/fingerprint 0.0.7 → 0.0.8
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 +133 -1
- package/fingerprint.client.js +399 -0
- package/fingerprint.js +233 -1
- package/package.json +61 -48
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ Once the challenge is solved, a clearance "ticket" is issued via a secure cookie
|
|
|
34
34
|
- **Pluggable Datastore**: Supports external datastores like Redis for state persistence and scalability across multiple server instances.
|
|
35
35
|
- **Express.js Middleware**: Easy integration into an Express application with `powMiddleware`. The datastore must support setting a Time-To-Live (TTL) for challenge secrets.
|
|
36
36
|
- **Timing Attack Protection**: Uses `crypto.timingSafeEqual` for secure ticket validation.
|
|
37
|
+
- **Bot Whitelisting**: Includes a DNS-based verification mechanism to reliably identify and whitelist legitimate crawlers like Googlebot and Bingbot, preventing them from being challenged. The results are cached for optimal performance.
|
|
37
38
|
- **Automatic Parameter Tuning**: Includes a genetic algorithm-based optimizer (`startThresholdAutoTuning`) that analyzes real traffic to dynamically adjust not only suspicion thresholds (`low`, `medium`, `high`) but also the parameters for behavioral pattern detection, improving accuracy and reducing false positives over time.
|
|
38
39
|
|
|
39
40
|
## Installation and Usage
|
|
@@ -60,7 +61,7 @@ The `powMiddleware` requires a configuration object defining the weights of susp
|
|
|
60
61
|
import express from 'express';
|
|
61
62
|
import bodyParser from 'body-parser';
|
|
62
63
|
import cookieParser from 'cookie-parser';
|
|
63
|
-
import { powMiddleware
|
|
64
|
+
import { powMiddleware, default_whitelist } from './fingerprint.js'; // Adjust the path
|
|
64
65
|
|
|
65
66
|
const app = express();
|
|
66
67
|
app.use(cookieParser());
|
|
@@ -132,6 +133,26 @@ const securityConfig = {
|
|
|
132
133
|
}
|
|
133
134
|
]
|
|
134
135
|
},
|
|
136
|
+
// (Optional) Whitelisting configuration.
|
|
137
|
+
whitelist: [
|
|
138
|
+
// Option 1: Static IP Allowlist.
|
|
139
|
+
// A simple array of IPs or CIDR ranges that are always allowed, bypassing all checks.
|
|
140
|
+
// Useful for internal tools, trusted partners, or monitoring services.
|
|
141
|
+
// This check is performed first for maximum efficiency.
|
|
142
|
+
{ type: 'allowlist', entries: [
|
|
143
|
+
'192.168.1.100', // A specific internal IP
|
|
144
|
+
'203.0.113.0/24', // A partner's network range
|
|
145
|
+
'2001:db8::/32' // An IPv6 range
|
|
146
|
+
]},
|
|
147
|
+
// Option 2: DNS-verified bots (e.g., search engine crawlers).
|
|
148
|
+
// This uses a secure DNS lookup (reverse then forward) to verify the bot's identity.
|
|
149
|
+
// The result is cached per IP to avoid repeated DNS lookups.
|
|
150
|
+
// You can use the provided default list, which contains over 50 common bots, and extend it.
|
|
151
|
+
...default_whitelist(), // Use the defaults
|
|
152
|
+
{ userAgent: 'MyIndustrySpecificBot', hostnameSuffix: '.my-bot-verifier.com' }, // Add a custom bot
|
|
153
|
+
],
|
|
154
|
+
// Or, if you only want the defaults:
|
|
155
|
+
// whitelist: default_whitelist(),
|
|
135
156
|
// The logger is required for auto-tuning. It collects data on requests.
|
|
136
157
|
logger: (log) => trafficData.push(log),
|
|
137
158
|
// (Optional) Configuration for the automatic threshold and pattern tuning.
|
|
@@ -245,6 +266,117 @@ const signature = generateRequestSignature({ action: 'update', id: 123 });
|
|
|
245
266
|
**Security Note:** This function is powerful but should be used with caution. The `secret` must be managed securely. It is typically used with a temporary, single-use secret provided by the server for a specific action, rather than a long-lived shared secret embedded in the client-side code.
|
|
246
267
|
|
|
247
268
|
---
|
|
269
|
+
|
|
270
|
+
## Why Use the Client-Side Library? The Client + Server Synergy
|
|
271
|
+
|
|
272
|
+
At first glance, client-side checks might seem redundant with server-side honeypots and analysis. In reality, they form two complementary and synergistic lines of defense.
|
|
273
|
+
|
|
274
|
+
Imagine your server is a fortified castle:
|
|
275
|
+
|
|
276
|
+
- **Server-Side Defense (the guards on the walls):** They inspect anyone who knocks on the gate. They are effective, but this means the enemy is already at your door, and your resources (guards) are mobilized for every interaction, legitimate or not.
|
|
277
|
+
- **Client-Side Defense (scouts and traps in the forest):** They detect suspicious movements and neutralize threats *before* they even reach the castle walls. This saves the castle's resources for genuine visitors.
|
|
278
|
+
|
|
279
|
+
The client-side library (`fingerprint.client.js`) gives your server "eyes and ears" where it was previously blind, offering three major advantages:
|
|
280
|
+
|
|
281
|
+
1. **Early Detection & Resource Savings:** A bot filling a client-side honeypot is flagged in its own browser. The server can then immediately block it based on the `X-Behavior-Metrics` header, saving CPU, memory, and bandwidth that would have been wasted processing a malicious request.
|
|
282
|
+
2. **Richer Behavioral Data:** The server cannot see how a user interacts with a page. The client-side library can detect non-human behavior (no mouse movement, instant form fills) that is impossible to spot from the server alone.
|
|
283
|
+
3. **More Robust Fingerprinting:** Server-side signals (IP, User-Agent) are easy to spoof. Client-side fingerprinting adds much stronger, hardware-based signals (Canvas, WebGL, CPU cores) that are significantly harder for bots to fake consistently.
|
|
284
|
+
|
|
285
|
+
### Strengths at a Glance
|
|
286
|
+
|
|
287
|
+
| Feature | Server-Side Only Approach | Client + Server Approach (with the library) |
|
|
288
|
+
| :---------------------- | :------------------------------------------------------ | :------------------------------------------------------------------------- |
|
|
289
|
+
| **Detection Point** | **Reactive** (after the request is received) | **Proactive** (before or during the request) |
|
|
290
|
+
| **Resource Usage** | Server is engaged for every request, good or bad. | Client-side pre-filtering saves significant server resources. |
|
|
291
|
+
| **Behavioral Analysis** | Limited to request velocity and sequence. | Rich data: mouse movement, typing rhythm, interaction with hidden elements. |
|
|
292
|
+
| **Fingerprint Evasion** | **Easy** for bots to spoof headers and rotate IPs. | **Hard** for bots to fake hardware-level fingerprints (Canvas/WebGL). |
|
|
293
|
+
| **Overall Strategy** | Guards at the gate. | Scouts in the field + Guards at the gate. |
|
|
294
|
+
|
|
295
|
+
In short, the client-side library is not an alternative, but a **force multiplier** for the server-side defenses.
|
|
296
|
+
|
|
297
|
+
### Client-Side Integration: The `initializeClient` function
|
|
298
|
+
|
|
299
|
+
To simplify setup, all client-side features can be enabled and configured through a single, unified function: `initializeClient(config)`. This is the recommended approach.
|
|
300
|
+
|
|
301
|
+
```javascript
|
|
302
|
+
import { initializeClient } from './path/to/fingerprint.client.js';
|
|
303
|
+
|
|
304
|
+
// --- EXAMPLES ---
|
|
305
|
+
|
|
306
|
+
// Example 1: Enable all default protections and protect same-origin fetch requests.
|
|
307
|
+
initializeClient({
|
|
308
|
+
honeypots: ['email_confirm', 'user_nickname'], // Your honeypot field names
|
|
309
|
+
fetch: {} // An empty object enables fetch protection for same-origin
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Example 2: Enable everything and protect a specific API domain.
|
|
313
|
+
initializeClient({
|
|
314
|
+
honeypots: ['email_confirm'],
|
|
315
|
+
fetch: {
|
|
316
|
+
targetDomains: ['api.yourdomain.com']
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Example 3: Enable only mouse and keystroke tracking, without patching fetch.
|
|
321
|
+
initializeClient({
|
|
322
|
+
mouse: true,
|
|
323
|
+
keystrokes: true
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Example 4: Disable keystroke tracking but keep other defaults.
|
|
327
|
+
initializeClient({
|
|
328
|
+
keystrokes: false,
|
|
329
|
+
fetch: {}
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Client-Side Behavioral Analysis
|
|
334
|
+
|
|
335
|
+
The following functions, available in `fingerprint.client.js`, allow for proactive, client-side detection of bot-like behavior. They collect metrics on user interaction which can be sent to the server for more accurate suspicion scoring. The server-side logic to interpret these metrics (via the `X-Behavior-Metrics` header) would need to be implemented as part of a custom scoring extension.
|
|
336
|
+
|
|
337
|
+
#### `startKeystrokeDynamicsTracker()`
|
|
338
|
+
*Client-side function only.* Starts tracking the timing between keystrokes. The average latency between key presses is a strong behavioral indicator. Humans have a natural, somewhat variable typing rhythm, whereas bots often simulate keystrokes with a fixed, unnaturally consistent delay, or paste text instantly (zero latency).
|
|
339
|
+
|
|
340
|
+
#### `startMouseEntropyTracker()`
|
|
341
|
+
*Client-side function only.* Starts tracking mouse movements on the page. It calculates a simple entropy score based on movement patterns. Human mouse movements are typically chaotic, whereas bots often have linear or no movement at all. This should be called once when your application's main component mounts.
|
|
342
|
+
|
|
343
|
+
#### `initializeHoneypots(fieldNames)`
|
|
344
|
+
*Client-side function only.* Sets up "traps" on hidden form fields. If a script automatically fills one of these fields, it's immediately flagged as a bot on the client side.
|
|
345
|
+
|
|
346
|
+
This provides a proactive, first-line defense against simple bots. By setting up traps directly in the browser, you can detect a bot the moment it interacts with a hidden field, rather than waiting for it to submit a form and consume server resources. This detection is then reported to the server via the `X-Behavior-Metrics` header, allowing for an immediate and efficient block.
|
|
347
|
+
|
|
348
|
+
- `fieldNames`: An array of strings corresponding to the `name` attributes of the honeypot input fields in your HTML.
|
|
349
|
+
|
|
350
|
+
### Advanced: Manual Wrapping with `protectedFetch`
|
|
351
|
+
|
|
352
|
+
If you prefer not to modify global functions or need fine-grained control over which requests are protected, you can use the `protectedFetch` wrapper. You must use this function instead of the standard `fetch` for your API calls.
|
|
353
|
+
|
|
354
|
+
- **`protectedFetch(resource, options)`**: A wrapper around the native `fetch` API that automatically enriches requests with security headers. It adds:
|
|
355
|
+
- `X-Device-Fingerprint`: The client's device fingerprint.
|
|
356
|
+
- `X-Behavior-Metrics`: A JSON string containing the metrics collected by the behavioral trackers (e.g., mouse entropy, honeypot interaction).
|
|
357
|
+
|
|
358
|
+
**Example:**
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
import {
|
|
362
|
+
initializeClient,
|
|
363
|
+
protectedFetch
|
|
364
|
+
} from './path/to/fingerprint.client.js';
|
|
365
|
+
|
|
366
|
+
// Start tracking user behavior as soon as the app loads.
|
|
367
|
+
// Note: You still need to initialize the trackers even if you use protectedFetch manually.
|
|
368
|
+
initializeClient({ fetch: false }); // Disables automatic fetch patching
|
|
369
|
+
|
|
370
|
+
// Now, use protectedFetch for your specific API calls.
|
|
371
|
+
async function submitForm(data) {
|
|
372
|
+
const response = await protectedFetch('/api/submit-data', {
|
|
373
|
+
method: 'POST',
|
|
374
|
+
body: JSON.stringify(data),
|
|
375
|
+
headers: {'Content-Type': 'application/json'}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
248
380
|
## Advanced Features
|
|
249
381
|
|
|
250
382
|
### Architecture: `FingerprintEngine`
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { cyrb53, FingerprintBuilder } from './fingerprint.builder.js';
|
|
2
|
+
|
|
3
|
+
const ClientLibrary = {
|
|
4
|
+
// Cache pour éviter de recalculer les constantes (Hardware, etc.)
|
|
5
|
+
_cachedBuilder: null,
|
|
6
|
+
/**
|
|
7
|
+
* Génère l'empreinte de l'appareil actuel.
|
|
8
|
+
*/
|
|
9
|
+
getDeviceFingerprint() {
|
|
10
|
+
if (typeof window === "undefined") {
|
|
11
|
+
console.error("getDeviceFingerprint can only be called on the client-side.");
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!this._cachedBuilder) {
|
|
16
|
+
const nav = window.navigator;
|
|
17
|
+
const screen = window.screen;
|
|
18
|
+
|
|
19
|
+
this._cachedBuilder = new FingerprintBuilder();
|
|
20
|
+
|
|
21
|
+
// 1. Hardware (Très stable) : Cœurs, RAM, GPU (si dispo via canvas), Touch
|
|
22
|
+
this._cachedBuilder.add(
|
|
23
|
+
"hw",
|
|
24
|
+
`${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// 2. Geo/Locale (Stable sauf voyage/VPN) : Timezone, Langue
|
|
28
|
+
this._cachedBuilder.add(
|
|
29
|
+
"geo",
|
|
30
|
+
`${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// 3. Screen (Stable sauf changement moniteur/zoom) : Dimensions, ColorDepth
|
|
34
|
+
this._cachedBuilder.add(
|
|
35
|
+
"scr",
|
|
36
|
+
`${screen.width}x${screen.height}_${screen.colorDepth}`,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// 4. Platform (Stable) : OS, Engine
|
|
40
|
+
this._cachedBuilder.add("os", nav.platform);
|
|
41
|
+
|
|
42
|
+
// 5. Graphics (WebGL Vendor/Renderer) - Invariant matériel fort
|
|
43
|
+
try {
|
|
44
|
+
const canvas = document.createElement("canvas");
|
|
45
|
+
const gl =
|
|
46
|
+
canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
47
|
+
if (gl) {
|
|
48
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
49
|
+
if (debugInfo) {
|
|
50
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
51
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
52
|
+
this._cachedBuilder.add("gpu", `${vendor}_${renderer}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 6. Canvas Fingerprinting (Rendering quirks)
|
|
59
|
+
try {
|
|
60
|
+
const canvas = document.createElement("canvas");
|
|
61
|
+
const ctx = canvas.getContext("2d");
|
|
62
|
+
if (ctx) {
|
|
63
|
+
canvas.width = 200;
|
|
64
|
+
canvas.height = 50;
|
|
65
|
+
ctx.textBaseline = "alphabetic";
|
|
66
|
+
ctx.font = "14px 'Arial'";
|
|
67
|
+
ctx.fillStyle = "#f60";
|
|
68
|
+
ctx.fillRect(125, 1, 62, 20);
|
|
69
|
+
ctx.fillStyle = "#069";
|
|
70
|
+
ctx.fillText("fingerprint", 2, 15);
|
|
71
|
+
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
72
|
+
ctx.fillText("fingerprint", 4, 17);
|
|
73
|
+
this._cachedBuilder.add("cvs", canvas.toDataURL());
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 7. Bot Detection (Indication cachée)
|
|
79
|
+
if (nav.webdriver) this._cachedBuilder.add("bot", "true");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return this._cachedBuilder.toString();
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Génère une signature de requête incluant le contexte.
|
|
87
|
+
* @param {object} payload
|
|
88
|
+
*/
|
|
89
|
+
generateRequestSignature(payload = {}) {
|
|
90
|
+
const deviceFp = this.getDeviceFingerprint();
|
|
91
|
+
const sortedPayload = Object.keys(payload)
|
|
92
|
+
.sort()
|
|
93
|
+
.map((k) => `${k}=${payload[k]}`)
|
|
94
|
+
.join("&");
|
|
95
|
+
const payloadHash = cyrb53(sortedPayload);
|
|
96
|
+
return `${deviceFp}|req:${payloadHash}`;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Génère une signature HMAC-SHA256 en utilisant l'API Web Crypto.
|
|
101
|
+
* @param {object} payload - Les données à signer.
|
|
102
|
+
* @param {string} secret - La clé secrète partagée.
|
|
103
|
+
* @returns {Promise<string>} La signature hexadécimale.
|
|
104
|
+
*/
|
|
105
|
+
async generateClientSideSignature(payload, secret) {
|
|
106
|
+
const sortedPayload = Object.keys(payload).sort().map((k) => `${k}=${payload[k]}`).join("&");
|
|
107
|
+
const encoder = new TextEncoder();
|
|
108
|
+
const key = await window.crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
109
|
+
name: "HMAC",
|
|
110
|
+
hash: "SHA-256"
|
|
111
|
+
}, false, ["sign"]);
|
|
112
|
+
const signatureBuffer = await window.crypto.subtle.sign("HMAC", key, encoder.encode(sortedPayload));
|
|
113
|
+
const hashArray = Array.from(new Uint8Array(signatureBuffer));
|
|
114
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @internal
|
|
119
|
+
* Resets the cached fingerprint builder. Used for testing purposes.
|
|
120
|
+
*/
|
|
121
|
+
_resetCache() {
|
|
122
|
+
this._cachedBuilder = null;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Démarre le suivi des mouvements de la souris pour calculer l'entropie.
|
|
127
|
+
* À appeler une fois sur la page.
|
|
128
|
+
*/
|
|
129
|
+
startMouseEntropyTracker() {
|
|
130
|
+
// S'assurer de ne pas attacher l'écouteur plusieurs fois
|
|
131
|
+
if (mouseMovements > 0) return;
|
|
132
|
+
|
|
133
|
+
document.addEventListener('mousemove', (e) => {
|
|
134
|
+
const dx = e.clientX - lastMousePos.x;
|
|
135
|
+
const dy = e.clientY - lastMousePos.y;
|
|
136
|
+
// Une métrique simple : la somme des distances. Un bot aura souvent 0.
|
|
137
|
+
metrics.mouseEntropy += Math.sqrt(dx * dx + dy * dy);
|
|
138
|
+
lastMousePos = {x: e.clientX, y: e.clientY};
|
|
139
|
+
mouseMovements++;
|
|
140
|
+
}, {passive: true});
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Démarre le suivi de la dynamique de frappe pour calculer la latence.
|
|
145
|
+
* À appeler une fois sur la page.
|
|
146
|
+
*/
|
|
147
|
+
startKeystrokeDynamicsTracker() {
|
|
148
|
+
// S'assurer de ne pas attacher l'écouteur plusieurs fois
|
|
149
|
+
if (keystrokeTimestamps.length > 0) return;
|
|
150
|
+
|
|
151
|
+
document.addEventListener('keydown', () => {
|
|
152
|
+
const now = performance.now();
|
|
153
|
+
if (keystrokeTimestamps.length > 0) {
|
|
154
|
+
const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
|
|
155
|
+
const latency = now - lastTimestamp;
|
|
156
|
+
// On ignore les latences irréalistes (trop longues ou trop courtes)
|
|
157
|
+
if (latency > 10 && latency < 2000) { // Augmenté à 2s
|
|
158
|
+
if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
|
|
159
|
+
keystrokeLatencies.shift(); // Garder la taille de l'historique
|
|
160
|
+
}
|
|
161
|
+
keystrokeLatencies.push(latency);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
keystrokeTimestamps.push(now);
|
|
165
|
+
}, {passive: true});
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Initialise ou réinitialise les honeypots côté client pour une détection immédiate.
|
|
170
|
+
* Les anciens écouteurs sont supprimés avant d'en ajouter de nouveaux.
|
|
171
|
+
* @param {string[]} honeypotFieldNames - Noms des champs de formulaire cachés.
|
|
172
|
+
*/
|
|
173
|
+
initializeHoneypots(honeypotFieldNames) {
|
|
174
|
+
// 1. Nettoyer les anciens écouteurs
|
|
175
|
+
activeHoneypotListeners.forEach((listener, field) => {
|
|
176
|
+
field.removeEventListener('input', listener);
|
|
177
|
+
});
|
|
178
|
+
activeHoneypotListeners.clear();
|
|
179
|
+
|
|
180
|
+
// 2. Ajouter les nouveaux écouteurs
|
|
181
|
+
honeypotFieldNames.forEach(fieldName => {
|
|
182
|
+
const field = document.querySelector(`[name="${fieldName}"]`);
|
|
183
|
+
if (field) {
|
|
184
|
+
// On utilise une fonction nommée (ou une référence) pour pouvoir la supprimer plus tard.
|
|
185
|
+
// L'option { once: true } est excellente, mais pour une réinitialisation complète,
|
|
186
|
+
// il est plus propre de gérer le nettoyage nous-mêmes.
|
|
187
|
+
const listener = () => {
|
|
188
|
+
this.onHoneypotTrigger();
|
|
189
|
+
// Se supprime lui-même après exécution, comme { once: true }
|
|
190
|
+
field.removeEventListener('input', listener);
|
|
191
|
+
};
|
|
192
|
+
field.addEventListener('input', listener);
|
|
193
|
+
activeHoneypotListeners.set(field, listener); // On stocke la référence
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Récupère les métriques comportementales collectées.
|
|
200
|
+
* À appeler avant d'envoyer une requête sensible.
|
|
201
|
+
* @returns {ClientBehaviorMetrics}
|
|
202
|
+
*/
|
|
203
|
+
getClientBehaviorMetrics() {
|
|
204
|
+
// Normalise l'entropie de la souris
|
|
205
|
+
if (mouseMovements > 10) {
|
|
206
|
+
metrics.mouseEntropy /= mouseMovements;
|
|
207
|
+
}
|
|
208
|
+
// Calcule la latence moyenne des frappes
|
|
209
|
+
if (keystrokeLatencies.length > 0) {
|
|
210
|
+
const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
|
|
211
|
+
metrics.keystrokeLatency = sum / keystrokeLatencies.length;
|
|
212
|
+
} else {
|
|
213
|
+
metrics.keystrokeLatency = 0;
|
|
214
|
+
}
|
|
215
|
+
return metrics;
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Enrichit une requête fetch avec les en-têtes de fingerprinting et de comportement.
|
|
220
|
+
* @param {RequestInfo} resource
|
|
221
|
+
* @param {RequestInit} [options]
|
|
222
|
+
* @returns {Promise<Response>}
|
|
223
|
+
*/
|
|
224
|
+
async protectedFetch(resource, options = {}) {
|
|
225
|
+
const fp = this.getDeviceFingerprint();
|
|
226
|
+
const behavior = this.getClientBehaviorMetrics();
|
|
227
|
+
|
|
228
|
+
const headers = new Headers(options.headers || {});
|
|
229
|
+
headers.set('X-Device-Fingerprint', fp);
|
|
230
|
+
headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
|
|
231
|
+
|
|
232
|
+
options.headers = headers;
|
|
233
|
+
return fetch(resource, options);
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
// --- Système d'interception de Fetch robuste et anti-conflit ---
|
|
237
|
+
|
|
238
|
+
_isFetchPatched: false,
|
|
239
|
+
_interceptorChain: [],
|
|
240
|
+
_originalFetch: (typeof window !== 'undefined') ? window.fetch : () => Promise.reject(new Error('fetch is not available')),
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Adds an interceptor function to the `fetch` chain.
|
|
244
|
+
* Chaque intercepteur reçoit `resource`, `options`, et une fonction `next`.
|
|
245
|
+
* Il DOIT appeler `next(resource, options)` pour continuer la chaîne.
|
|
246
|
+
* @param {function(RequestInfo, RequestInit, function): Promise<Response>} interceptor
|
|
247
|
+
*/
|
|
248
|
+
addFetchInterceptor(interceptor) {
|
|
249
|
+
if (!this._isFetchPatched) {
|
|
250
|
+
this.patchGlobalFetch();
|
|
251
|
+
}
|
|
252
|
+
this._interceptorChain.push(interceptor);
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
patchGlobalFetch() {
|
|
256
|
+
if (this._isFetchPatched || typeof window === 'undefined') return;
|
|
257
|
+
|
|
258
|
+
this._isFetchPatched = true;
|
|
259
|
+
window.fetch = (resource, options) => {
|
|
260
|
+
// Le "dispatcher" qui exécute la chaîne.
|
|
261
|
+
const dispatch = (index, res, opts) => {
|
|
262
|
+
if (index >= this._interceptorChain.length) {
|
|
263
|
+
// Fin de la chaîne, on appelle le fetch original.
|
|
264
|
+
return this._originalFetch(res, opts);
|
|
265
|
+
}
|
|
266
|
+
const nextInterceptor = this._interceptorChain[index];
|
|
267
|
+
// Appelle l'intercepteur actuel en lui passant la fonction pour appeler le suivant.
|
|
268
|
+
return nextInterceptor(res, opts, (nextRes, nextOpts) => dispatch(index + 1, nextRes, nextOpts));
|
|
269
|
+
};
|
|
270
|
+
return dispatch(0, resource, options || {});
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* La fonction qui est appelée lorsqu'un honeypot est déclenché.
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
onHoneypotTrigger : () => {
|
|
279
|
+
metrics.honeypotInteraction = true;
|
|
280
|
+
// On pourrait même envoyer un signalement au serveur immédiatement.
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Initialise l'intercepteur de fingerprinting.
|
|
285
|
+
* Il s'ajoute à la chaîne d'interception sans écraser les autres.
|
|
286
|
+
* @param {string[]} [targetDomains] - Optionnel. Liste de domaines à protéger.
|
|
287
|
+
* Si non fourni, protège les requêtes de même origine.
|
|
288
|
+
*/
|
|
289
|
+
initializeFetch(targetDomains = []) {
|
|
290
|
+
const fingerprintInterceptor = (resource, options, next) => {
|
|
291
|
+
const requestUrl = (resource instanceof Request) ? resource.url : String(resource);
|
|
292
|
+
let shouldProtect = false;
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const url = new URL(requestUrl, window.location.origin);
|
|
296
|
+
// Protéger si la liste de domaines est vide ET que la requête est de même origine,
|
|
297
|
+
// OU si le domaine de la requête est dans la liste fournie.
|
|
298
|
+
shouldProtect = (targetDomains.length === 0 && url.origin === window.location.origin) ||
|
|
299
|
+
(targetDomains.length > 0 && targetDomains.includes(url.hostname));
|
|
300
|
+
} catch (e) {
|
|
301
|
+
// Si l'URL est relative (ex: '/api/data'), new URL() ne lèvera pas d'erreur.
|
|
302
|
+
// Ce bloc est une sécurité pour les cas où l'URL serait malformée.
|
|
303
|
+
// On protège par défaut si aucune liste de domaines n'est spécifiée.
|
|
304
|
+
shouldProtect = targetDomains.length === 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (shouldProtect) {
|
|
308
|
+
const fp = this.getDeviceFingerprint();
|
|
309
|
+
const behavior = this.getClientBehaviorMetrics();
|
|
310
|
+
const headers = new Headers(options.headers || {});
|
|
311
|
+
headers.set('X-Device-Fingerprint', fp);
|
|
312
|
+
headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
|
|
313
|
+
options.headers = headers;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Passe la main à l'intercepteur suivant dans la chaîne.
|
|
317
|
+
return next(resource, options);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
this.addFetchInterceptor(fingerprintInterceptor);
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* @typedef {object} ClientConfig
|
|
325
|
+
* @property {boolean} [mouse=true] - Activer le suivi de l'entropie de la souris.
|
|
326
|
+
* @property {boolean} [keystrokes=true] - Activer le suivi de la dynamique de frappe.
|
|
327
|
+
* @property {string[]} [honeypots] - Noms des champs de formulaire honeypot à initialiser.
|
|
328
|
+
* @property {object} [fetch] - Configuration pour l'interception de fetch.
|
|
329
|
+
* @property {string[]} [fetch.targetDomains] - Domaines à protéger. Si non fourni, protège les requêtes de même origine.
|
|
330
|
+
*/
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Initialise toutes les protections côté client en une seule fois.
|
|
334
|
+
* C'est la méthode d'initialisation recommandée.
|
|
335
|
+
* @param {ClientConfig} [config={}] - L'objet de configuration.
|
|
336
|
+
*/
|
|
337
|
+
initializeClient(config = {}) {
|
|
338
|
+
const {
|
|
339
|
+
mouse = true,
|
|
340
|
+
keystrokes = true,
|
|
341
|
+
honeypots = [],
|
|
342
|
+
fetch: fetchConfig,
|
|
343
|
+
} = config;
|
|
344
|
+
|
|
345
|
+
if (mouse) {
|
|
346
|
+
this.startMouseEntropyTracker();
|
|
347
|
+
}
|
|
348
|
+
if (keystrokes) {
|
|
349
|
+
this.startKeystrokeDynamicsTracker();
|
|
350
|
+
}
|
|
351
|
+
if (honeypots.length > 0) {
|
|
352
|
+
this.initializeHoneypots(honeypots);
|
|
353
|
+
}
|
|
354
|
+
if (fetchConfig !== undefined) {
|
|
355
|
+
this.initializeFetch(fetchConfig.targetDomains);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @typedef {object} ClientBehaviorMetrics
|
|
362
|
+
* @property {number} mouseEntropy - Entropie des mouvements de la souris.
|
|
363
|
+
* @property {number} keystrokeLatency - Latence moyenne entre les frappes.
|
|
364
|
+
* @property {boolean} honeypotInteraction - Vrai si un honeypot a été touché.
|
|
365
|
+
*/
|
|
366
|
+
|
|
367
|
+
/** @type {ClientBehaviorMetrics} */
|
|
368
|
+
const metrics = {
|
|
369
|
+
mouseEntropy: 0,
|
|
370
|
+
keystrokeLatency: 0,
|
|
371
|
+
honeypotInteraction: false,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
let lastMousePos = { x: 0, y: 0 };
|
|
375
|
+
let mouseMovements = 0;
|
|
376
|
+
let activeHoneypotListeners = new Map(); // Garde une trace des écouteurs actifs
|
|
377
|
+
let keystrokeTimestamps = [];
|
|
378
|
+
let keystrokeLatencies = []; // NOUVEAU: Tableau dédié pour les latences
|
|
379
|
+
const KEYSTROKE_HISTORY_MAX = 20; // On garde l'historique des 20 dernières frappes
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
// Exporter les fonctions individuellement pour la compatibilité ascendante
|
|
384
|
+
export const getDeviceFingerprint = ClientLibrary.getDeviceFingerprint.bind(ClientLibrary);
|
|
385
|
+
export const generateRequestSignature = ClientLibrary.generateRequestSignature.bind(ClientLibrary);
|
|
386
|
+
export const generateClientSideSignature = ClientLibrary.generateClientSideSignature.bind(ClientLibrary);
|
|
387
|
+
export const _resetCache = ClientLibrary._resetCache.bind(ClientLibrary);
|
|
388
|
+
export const startMouseEntropyTracker = ClientLibrary.startMouseEntropyTracker.bind(ClientLibrary);
|
|
389
|
+
export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamicsTracker.bind(ClientLibrary);
|
|
390
|
+
export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
|
|
391
|
+
export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
|
|
392
|
+
export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
|
|
393
|
+
export const addFetchInterceptor = ClientLibrary.addFetchInterceptor.bind(ClientLibrary);
|
|
394
|
+
export const patchGlobalFetch = ClientLibrary.patchGlobalFetch.bind(ClientLibrary);
|
|
395
|
+
export const initializeFetch = ClientLibrary.initializeFetch.bind(ClientLibrary);
|
|
396
|
+
export const initializeClient = ClientLibrary.initializeClient.bind(ClientLibrary);
|
|
397
|
+
|
|
398
|
+
// Export the internal object for testing purposes
|
|
399
|
+
export default ClientLibrary;
|
package/fingerprint.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// C:/Dev/games.primals.net/src/utils/fingerprint.js
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
+
import { parse } from "node:net";
|
|
4
|
+
import dns from "node:dns/promises";
|
|
3
5
|
import { Optimization } from "./library.js";
|
|
4
6
|
import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
|
|
5
7
|
|
|
@@ -1124,14 +1126,134 @@ export class FingerprintEngine {
|
|
|
1124
1126
|
this.isProduction = isProduction;
|
|
1125
1127
|
}
|
|
1126
1128
|
|
|
1129
|
+
/**
|
|
1130
|
+
* Checks if an IP address is in the static allowlist (IPs or CIDR ranges).
|
|
1131
|
+
* This is the fastest check and should be performed first.
|
|
1132
|
+
* @private
|
|
1133
|
+
* @param {string} clientIp - The IP address of the client.
|
|
1134
|
+
* @returns {boolean} True if the IP is in the allowlist.
|
|
1135
|
+
*/
|
|
1136
|
+
_isIpInAllowlist(clientIp) {
|
|
1137
|
+
const { whitelist = [] } = this.securityConfig;
|
|
1138
|
+
const allowlistRule = whitelist.find(rule => rule.type === 'allowlist');
|
|
1139
|
+
|
|
1140
|
+
if (!allowlistRule || !allowlistRule.entries || allowlistRule.entries.length === 0) {
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const ip = parse(clientIp);
|
|
1145
|
+
const ipVersion = ip.family;
|
|
1146
|
+
|
|
1147
|
+
for (const entry of allowlistRule.entries) {
|
|
1148
|
+
if (entry.includes('/')) { // CIDR range
|
|
1149
|
+
try {
|
|
1150
|
+
const [range, prefixStr] = entry.split('/');
|
|
1151
|
+
const prefix = parseInt(prefixStr, 10);
|
|
1152
|
+
const rangeIp = parse(range);
|
|
1153
|
+
|
|
1154
|
+
if (rangeIp.family !== ipVersion) continue;
|
|
1155
|
+
|
|
1156
|
+
const ipBytes = ip.toBuffer();
|
|
1157
|
+
const rangeBytes = rangeIp.toBuffer();
|
|
1158
|
+
const mask = Buffer.alloc(ipBytes.length, 0xff);
|
|
1159
|
+
|
|
1160
|
+
for (let i = 0; i < Math.floor(prefix / 8); i++) {
|
|
1161
|
+
if (ipBytes[i] !== rangeBytes[i]) {
|
|
1162
|
+
break; // Mismatch in full byte, move to next entry
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
const remainingBits = prefix % 8;
|
|
1166
|
+
if (remainingBits > 0) {
|
|
1167
|
+
const byteIndex = Math.floor(prefix / 8);
|
|
1168
|
+
const bitmask = (0xff << (8 - remainingBits)) & 0xff;
|
|
1169
|
+
if ((ipBytes[byteIndex] & bitmask) !== (rangeBytes[byteIndex] & bitmask)) {
|
|
1170
|
+
continue; // Mismatch in partial byte
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return true; // IP is in CIDR range
|
|
1174
|
+
} catch (e) { continue; /* Ignore invalid CIDR entries */ }
|
|
1175
|
+
} else if (entry === clientIp) { // Direct IP match
|
|
1176
|
+
return true;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Verifies if a request comes from a legitimate, whitelisted bot (e.g., Googlebot)
|
|
1183
|
+
* using reverse and forward DNS lookups. The result is cached.
|
|
1184
|
+
* @private
|
|
1185
|
+
* @param {object} requestContext - The request context.
|
|
1186
|
+
* @returns {Promise<boolean>} True if the request is from a verified whitelisted bot.
|
|
1187
|
+
*/
|
|
1188
|
+
async _verifyWhitelistedBot(requestContext) {
|
|
1189
|
+
const { whitelist = [] } = this.securityConfig;
|
|
1190
|
+
const botRules = whitelist.filter(rule => rule.hostnameSuffix);
|
|
1191
|
+
if (botRules.length === 0) {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const { clientIp, headers } = requestContext;
|
|
1196
|
+
const userAgent = headers['user-agent'] || '';
|
|
1197
|
+
|
|
1198
|
+
const matchedRule = botRules.find(rule => {
|
|
1199
|
+
if (!rule.userAgent) return false;
|
|
1200
|
+
try {
|
|
1201
|
+
return new RegExp(rule.userAgent).test(userAgent);
|
|
1202
|
+
} catch (e) {
|
|
1203
|
+
console.error(`[Fingerprint] Invalid regex in whitelist rule: ${rule.userAgent}`);
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
if (!matchedRule) {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const cacheKey = `ip-whitelist:${clientIp}`;
|
|
1212
|
+
const cachedStatus = await store.get(cacheKey);
|
|
1213
|
+
|
|
1214
|
+
if (cachedStatus === 'verified') {
|
|
1215
|
+
return true;
|
|
1216
|
+
}
|
|
1217
|
+
if (cachedStatus === 'failed') {
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
try {
|
|
1222
|
+
// 1. Reverse DNS lookup
|
|
1223
|
+
const hostnames = await dns.reverse(clientIp);
|
|
1224
|
+
const validHostname = hostnames.find(h => h.endsWith(matchedRule.hostnameSuffix));
|
|
1225
|
+
|
|
1226
|
+
if (!validHostname) {
|
|
1227
|
+
await store.set(cacheKey, 'failed', 86400); // Cache failure for 24h
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// 2. Forward DNS lookup
|
|
1232
|
+
const addresses = await dns.resolve(validHostname);
|
|
1233
|
+
if (addresses.includes(clientIp)) {
|
|
1234
|
+
await store.set(cacheKey, 'verified', 86400); // Cache success for 24h
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
// DNS errors are common (e.g., for IPs with no rDNS record), treat as failure.
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
await store.set(cacheKey, 'failed', 86400); // Cache failure for 24h
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1127
1245
|
async processRequest(requestContext) {
|
|
1128
1246
|
const { clientIp, path, cookies, query, isStatic } = requestContext;
|
|
1129
1247
|
const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
|
|
1130
|
-
|
|
1131
1248
|
if (isStatic) {
|
|
1132
1249
|
return { action: 'next', score: 0, vector: {} };
|
|
1133
1250
|
}
|
|
1134
1251
|
|
|
1252
|
+
// 1. Check static IP allowlist first for maximum performance.
|
|
1253
|
+
if (this._isIpInAllowlist(clientIp)) {
|
|
1254
|
+
return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'allowlist' } };
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1135
1257
|
const { pow_nonce } = query;
|
|
1136
1258
|
|
|
1137
1259
|
// Honeypot: Direct probing of challenge endpoints is highly suspicious.
|
|
@@ -1145,6 +1267,11 @@ export class FingerprintEngine {
|
|
|
1145
1267
|
}
|
|
1146
1268
|
}
|
|
1147
1269
|
|
|
1270
|
+
// Check if the request is from a verified, whitelisted bot (e.g., Googlebot)
|
|
1271
|
+
if (await this._verifyWhitelistedBot(requestContext)) {
|
|
1272
|
+
return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'bot' } };
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1148
1275
|
// Check for persisted "condemned" status early.
|
|
1149
1276
|
const { deviceData } = await resolveRequestIdentity(requestContext);
|
|
1150
1277
|
if (deviceData?.condemned) {
|
|
@@ -1410,6 +1537,111 @@ export class FingerprintEngine {
|
|
|
1410
1537
|
}
|
|
1411
1538
|
}
|
|
1412
1539
|
|
|
1540
|
+
/**
|
|
1541
|
+
* Returns a default list of whitelisting rules for common and legitimate web crawlers.
|
|
1542
|
+
* This list can be used as a base and extended with custom rules.
|
|
1543
|
+
* @returns {Array<{userAgent: string, hostnameSuffix: string}>}
|
|
1544
|
+
*/
|
|
1545
|
+
export const default_whitelist = () => [
|
|
1546
|
+
// === Moteurs de recherche majeurs ===
|
|
1547
|
+
{ userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' },
|
|
1548
|
+
{ userAgent: 'Google-Extended', hostnameSuffix: '.google.com' },
|
|
1549
|
+
{ userAgent: 'AdsBot-Google', hostnameSuffix: '.googlebot.com' },
|
|
1550
|
+
{ userAgent: 'Mediapartners-Google', hostnameSuffix: '.google.com' },
|
|
1551
|
+
{ userAgent: 'Google-InspectionTool', hostnameSuffix: '.google.com' },
|
|
1552
|
+
{ userAgent: '(bingbot|adidxbot)', hostnameSuffix: '.search.msn.com' },
|
|
1553
|
+
{ userAgent: 'DuckDuckBot', hostnameSuffix: '.duckduckgo.com' },
|
|
1554
|
+
{ userAgent: 'YandexBot', hostnameSuffix: '.yandex.com' },
|
|
1555
|
+
{ userAgent: 'YandexImages', hostnameSuffix: '.yandex.com' },
|
|
1556
|
+
{ userAgent: 'Baiduspider', hostnameSuffix: '.crawl.baidu.com' },
|
|
1557
|
+
{ userAgent: 'Slurp', hostnameSuffix: '.crawl.yahoo.net' },
|
|
1558
|
+
{ userAgent: 'Sogou web spider', hostnameSuffix: '.sogou.com' },
|
|
1559
|
+
{ userAgent: 'Exabot', hostnameSuffix: '.exabot.com' },
|
|
1560
|
+
{ userAgent: 'ia_archiver', hostnameSuffix: '.alexa.com' },
|
|
1561
|
+
{ userAgent: 'SeznamBot', hostnameSuffix: '.seznam.cz' },
|
|
1562
|
+
{ userAgent: 'Mail.RU_Bot', hostnameSuffix: '.mail.ru' },
|
|
1563
|
+
{ userAgent: 'Yeti', hostnameSuffix: '.naver.com' }, // Naver
|
|
1564
|
+
|
|
1565
|
+
// === Outils SEO et d'analyse ===
|
|
1566
|
+
{ userAgent: 'AhrefsBot', hostnameSuffix: '.ahrefs.com' },
|
|
1567
|
+
{ userAgent: 'SemrushBot', hostnameSuffix: '.semrush.com' },
|
|
1568
|
+
{ userAgent: 'MJ12bot', hostnameSuffix: '.mj12bot.com' }, // Majestic
|
|
1569
|
+
{ userAgent: 'rogerbot', hostnameSuffix: '.moz.com' }, // Moz
|
|
1570
|
+
{ userAgent: 'DotBot', hostnameSuffix: '.moz.com' }, // Moz (anciennement opensiteexplorer.org)
|
|
1571
|
+
{ userAgent: 'Screaming Frog SEO Spider', hostnameSuffix: '.screamingfrog.co.uk' },
|
|
1572
|
+
{ userAgent: 'cognitiveseo', hostnameSuffix: '.cognitiveseo.com' },
|
|
1573
|
+
{ userAgent: 'SEOkicks', hostnameSuffix: '.seokicks.com' },
|
|
1574
|
+
{ userAgent: 'serpstatbot', hostnameSuffix: '.serpstatbot.com' },
|
|
1575
|
+
{ userAgent: 'MegaIndex', hostnameSuffix: '.megaindex.com' },
|
|
1576
|
+
{ userAgent: 'LinkpadBot', hostnameSuffix: '.linkpad.ru' },
|
|
1577
|
+
{ userAgent: 'Sistrix', hostnameSuffix: '.sistrix.com' },
|
|
1578
|
+
{ userAgent: 'RyteBot', hostnameSuffix: '.ryte.com' },
|
|
1579
|
+
{ userAgent: 'linkfluence', hostnameSuffix: '.linkfluence.com' },
|
|
1580
|
+
{ userAgent: 'TurnitinBot', hostnameSuffix: '.turnitin.com' },
|
|
1581
|
+
{ userAgent: 'GrapeshotCrawler', hostnameSuffix: '.grapeshot.co.uk' },
|
|
1582
|
+
|
|
1583
|
+
// === Robots d'IA et de données ===
|
|
1584
|
+
{ userAgent: 'GPTBot', hostnameSuffix: '.openai.com' },
|
|
1585
|
+
{ userAgent: 'ChatGPT-User', hostnameSuffix: '.openai.com' },
|
|
1586
|
+
{ userAgent: 'Applebot', hostnameSuffix: '.applebot.apple.com' },
|
|
1587
|
+
{ userAgent: 'CCBot', hostnameSuffix: '.commoncrawl.org' },
|
|
1588
|
+
{ userAgent: 'Bytespider', hostnameSuffix: '.bytespider.com' }, // ByteDance (TikTok)
|
|
1589
|
+
{ userAgent: 'Diffbot', hostnameSuffix: '.diffbot.com' },
|
|
1590
|
+
{ userAgent: 'PerplexityBot', hostnameSuffix: '.perplexity.ai' },
|
|
1591
|
+
{ userAgent: 'ClaudeBot', hostnameSuffix: '.anthropic.com' },
|
|
1592
|
+
{ userAgent: 'cohere.io', hostnameSuffix: '.cohere.io' },
|
|
1593
|
+
{ userAgent: 'DataForSeoBot', hostnameSuffix: '.dataforseo.com' },
|
|
1594
|
+
{ userAgent: 'YouBot', hostnameSuffix: '.you.com' },
|
|
1595
|
+
{ userAgent: 'omgili', hostnameSuffix: '.omgili.com' },
|
|
1596
|
+
|
|
1597
|
+
// === Réseaux sociaux et partage ===
|
|
1598
|
+
{ userAgent: 'facebookexternalhit', hostnameSuffix: '.facebook.com' },
|
|
1599
|
+
{ userAgent: 'facebot', hostnameSuffix: '.facebook.com' },
|
|
1600
|
+
{ userAgent: 'Twitterbot', hostnameSuffix: '.twttr.com' },
|
|
1601
|
+
{ userAgent: 'Pinterestbot', hostnameSuffix: '.pinterest.com' },
|
|
1602
|
+
{ userAgent: 'LinkedInBot', hostnameSuffix: '.linkedin.com' },
|
|
1603
|
+
{ userAgent: 'Slackbot', hostnameSuffix: '.slack.com' },
|
|
1604
|
+
{ userAgent: 'Discordbot', hostnameSuffix: '.discord.com' },
|
|
1605
|
+
{ userAgent: 'TelegramBot', hostnameSuffix: '.telegram.org' },
|
|
1606
|
+
{ userAgent: 'WhatsApp', hostnameSuffix: '.wa.me' },
|
|
1607
|
+
{ userAgent: 'SkypeUriPreview', hostnameSuffix: '.skype.com' },
|
|
1608
|
+
{ userAgent: 'redditbot', hostnameSuffix: '.reddit.com' },
|
|
1609
|
+
|
|
1610
|
+
// === Services de monitoring et d'uptime ===
|
|
1611
|
+
{ userAgent: 'UptimeRobot', hostnameSuffix: '.uptimerobot.com' },
|
|
1612
|
+
{ userAgent: 'Pingdom', hostnameSuffix: '.pingdom.com' },
|
|
1613
|
+
{ userAgent: 'StatusCake', hostnameSuffix: '.statuscake.com' },
|
|
1614
|
+
{ userAgent: 'Site24x7', hostnameSuffix: '.site24x7.com' },
|
|
1615
|
+
{ userAgent: 'Freshping', hostnameSuffix: '.freshping.io' },
|
|
1616
|
+
{ userAgent: 'Better Uptime', hostnameSuffix: '.betteruptime.com' },
|
|
1617
|
+
{ userAgent: 'Checkly', hostnameSuffix: '.checkly-infra.com' },
|
|
1618
|
+
{ userAgent: 'Datadog', hostnameSuffix: '.datadoghq.com' },
|
|
1619
|
+
{ userAgent: 'NewRelicPinger', hostnameSuffix: '.newrelic.com' },
|
|
1620
|
+
|
|
1621
|
+
// === Archives et agrégateurs de contenu ===
|
|
1622
|
+
{ userAgent: 'archive.org_bot', hostnameSuffix: '.archive.org' },
|
|
1623
|
+
{ userAgent: 'Feedly', hostnameSuffix: '.feedly.com' },
|
|
1624
|
+
{ userAgent: 'FeedFetcher-Google', hostnameSuffix: '.google.com' },
|
|
1625
|
+
{ userAgent: 'TheOldReader', hostnameSuffix: '.theoldreader.com' },
|
|
1626
|
+
{ userAgent: 'Inoreader', hostnameSuffix: '.inoreader.com' },
|
|
1627
|
+
{ userAgent: 'FlipboardProxy', hostnameSuffix: '.flipboard.com' },
|
|
1628
|
+
{ userAgent: 'PaperLiBot', hostnameSuffix: '.paper.li' },
|
|
1629
|
+
|
|
1630
|
+
// === Services Cloud et Plateformes ===
|
|
1631
|
+
{ userAgent: 'Amazon Route 53 Health Check', hostnameSuffix: '.amazonaws.com' },
|
|
1632
|
+
{ userAgent: 'Google-Cloud-Scheduler', hostnameSuffix: '.google.com' },
|
|
1633
|
+
{ userAgent: 'APIs-Google', hostnameSuffix: '.google.com' },
|
|
1634
|
+
|
|
1635
|
+
// === Divers ===
|
|
1636
|
+
{ userAgent: 'W3C_Validator', hostnameSuffix: '.w3.org' },
|
|
1637
|
+
{ userAgent: 'GTmetrix', hostnameSuffix: '.gtmetrix.com' },
|
|
1638
|
+
{ userAgent: 'WebPageTest', hostnameSuffix: '.webpagetest.org' },
|
|
1639
|
+
{ userAgent: 'Google-Site-Verification', hostnameSuffix: '.google.com' },
|
|
1640
|
+
{ userAgent: 'KeyCDN', hostnameSuffix: '.keycdn.com' },
|
|
1641
|
+
];
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
|
|
1413
1645
|
// --- Proof-of-Work Middleware (The Tollbooth) ---
|
|
1414
1646
|
export const powMiddleware = (securityConfig) => {
|
|
1415
1647
|
const engine = new FingerprintEngine(securityConfig);
|
package/package.json
CHANGED
|
@@ -1,48 +1,61 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.0.
|
|
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"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"fingerprint.js",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@anonympins/fingerprint",
|
|
3
|
+
"version": "0.0.8",
|
|
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"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"fingerprint.js",
|
|
15
|
+
"fingerprint.client.js",
|
|
16
|
+
"library.js",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/anonympins/fingerprint.git"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"fingerprint",
|
|
26
|
+
"bot",
|
|
27
|
+
"anti-bot",
|
|
28
|
+
"security",
|
|
29
|
+
"express",
|
|
30
|
+
"middleware",
|
|
31
|
+
"proof-of-work",
|
|
32
|
+
"pow",
|
|
33
|
+
"rate-limiting",
|
|
34
|
+
"mitigation",
|
|
35
|
+
"captcha",
|
|
36
|
+
"dns"
|
|
37
|
+
],
|
|
38
|
+
"author": "anonympins",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/anonympins/fingerprint/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/anonympins/fingerprint#readme",
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"body-parser": "^1.20.2",
|
|
46
|
+
"cookie-parser": "^1.4.6",
|
|
47
|
+
"express": "^4.18.2",
|
|
48
|
+
"vitest": "^4.1.11",
|
|
49
|
+
"ioredis": "^5.3.2",
|
|
50
|
+
"mongodb": "^6.3.0",
|
|
51
|
+
"prom-client": "^15.1.2"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"ioredis": "^5.3.2",
|
|
55
|
+
"mongodb": "^6.3.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"ioredis": { "optional": true },
|
|
59
|
+
"mongodb": { "optional": true }
|
|
60
|
+
}
|
|
61
|
+
}
|