@anonympins/fingerprint 0.5.1 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +118 -115
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +52 -13
- package/src/js/fingerprint.builder.js +169 -168
- package/src/js/fingerprint.client.js +1271 -1189
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +436 -162
- package/src/js/fingerprint.utils.js +30 -0
- package/src/js/gpu_pow.solver.js +96 -1
- package/src/js/library.js +40 -21
- package/src/js/mongodb-store.js +165 -79
- package/src/js/pow.solver.inline.js +627 -30
- package/src/js/pow.solver.js +453 -31
- package/src/js/problem-manager.js +1 -1
- package/src/js/redis-store.js +114 -42
- package/src/js/tests/dns-circuit-breaker.test.js +55 -0
- package/src/js/tests/fingerprint.client.init.test.js +143 -143
- package/src/js/tests/fingerprint.client.test.js +128 -128
- package/src/js/tests/fingerprint.engine.test.js +370 -370
- package/src/js/tests/fingerprint.test.js +119 -5
- package/src/js/tests/gpu_pow.test.js +81 -0
- package/src/js/tests/ip-reputation.test.js +1 -1
- package/src/js/tests/pow.solver.test.js +9 -6
- package/src/js/tests/usefulWorkSolver.test.js +162 -0
- package/src/php/Challenge/ChallengeUtils.php +137 -35
- package/src/php/FingerprintClient.php +22 -5
- package/src/php/FingerprintEngine.php +335 -58
- package/src/php/Optimization/Optimization.php +23 -16
- package/src/php/Optimization/OptimizationOperators.php +27 -0
- package/src/php/ProblemManager.php +1 -1
- package/src/php/Store/MongoDbStore.php +184 -104
- package/src/php/Store/RedisStore.php +91 -11
- package/src/php/Tests/ChallengeUtilsTest.php +285 -285
- package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
- package/src/php/Tests/IpReputationTest.php +1 -1
- package/src/php/Tests/PowTest.php +1 -1
- package/src/php/Tests/RequestUtilsTest.php +29 -0
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/RequestUtils.php +74 -21
- package/src/php/Utils/TLSClientHelloParser.php +212 -116
|
@@ -19,6 +19,36 @@ export function verifyZkpProof(yStr, tStr, sStr) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export function safeJsonStringify(val) {
|
|
23
|
+
return JSON.stringify(val)
|
|
24
|
+
.replace(/</g, '\\u003c')
|
|
25
|
+
.replace(/>/g, '\\u003e')
|
|
26
|
+
.replace(/\u2028/g, '\\u2028')
|
|
27
|
+
.replace(/\u2029/g, '\\u2029');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function sanitizeRedirectPath(p) {
|
|
31
|
+
if (typeof p !== 'string') return '/';
|
|
32
|
+
let sanitized = p.replace(/[^a-zA-Z0-9\/.\-_~%?&=:@+,;]/g, '');
|
|
33
|
+
|
|
34
|
+
// Prevent protocol-relative redirects (e.g. //evil.com)
|
|
35
|
+
if (sanitized.startsWith('//')) {
|
|
36
|
+
sanitized = '/' + sanitized.replace(/^\/+/g, '');
|
|
37
|
+
}
|
|
38
|
+
// Prevent absolute redirects (e.g. http://evil.com)
|
|
39
|
+
if (/^https?:\/\//i.test(sanitized)) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = new URL(sanitized);
|
|
42
|
+
sanitized = parsed.pathname + parsed.search + parsed.hash;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
sanitized = '/';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!sanitized.startsWith('/')) {
|
|
48
|
+
sanitized = '/' + sanitized;
|
|
49
|
+
}
|
|
50
|
+
return sanitized.replace(/^\/+/g, '/');
|
|
51
|
+
}
|
|
22
52
|
export function decodePolymorphicFingerprint(fpString, mapping) {
|
|
23
53
|
if (!fpString || !mapping || !mapping.keys) return fpString;
|
|
24
54
|
const reverseKeys = {};
|
package/src/js/gpu_pow.solver.js
CHANGED
|
@@ -24,6 +24,7 @@ export class GpuPowSolver {
|
|
|
24
24
|
}
|
|
25
25
|
} catch (e) {
|
|
26
26
|
console.warn('[GPU-PoW] WebGPU failed or disabled, falling back to WebGL2:', e);
|
|
27
|
+
// Fallthrough to WebGL2
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// Fallback to WebGL2
|
|
@@ -35,7 +36,20 @@ export class GpuPowSolver {
|
|
|
35
36
|
duration: performance.now() - start
|
|
36
37
|
};
|
|
37
38
|
} catch (e) {
|
|
38
|
-
|
|
39
|
+
console.warn('[GPU-PoW] WebGL2 failed or disabled, falling back to WebGL1:', e);
|
|
40
|
+
// Fallthrough to WebGL1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fallback to WebGL1
|
|
44
|
+
try {
|
|
45
|
+
const result = await this._solveWebGL1(numericSeed, iterations);
|
|
46
|
+
return {
|
|
47
|
+
solution: result,
|
|
48
|
+
platform: 'webgl1',
|
|
49
|
+
duration: performance.now() - start
|
|
50
|
+
};
|
|
51
|
+
} catch (e) {
|
|
52
|
+
throw new Error(`[GPU-PoW] All GPU solvers (WebGPU, WebGL2, WebGL1) failed: ${e.message}`);
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
55
|
|
|
@@ -200,6 +214,87 @@ export class GpuPowSolver {
|
|
|
200
214
|
return result.map(v => v.toFixed(6)).join(',');
|
|
201
215
|
}
|
|
202
216
|
|
|
217
|
+
static async _solveWebGL1(seed, iterations) {
|
|
218
|
+
const canvas = document.createElement('canvas');
|
|
219
|
+
canvas.width = 8;
|
|
220
|
+
canvas.height = 8; // 64 pixels total matching WebGPU size
|
|
221
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); // Get WebGL1 context with legacy fallback
|
|
222
|
+
if (!gl) throw new Error('WebGL1 context not supported.');
|
|
223
|
+
|
|
224
|
+
// WebGL1 Vertex Shader
|
|
225
|
+
const vs = `
|
|
226
|
+
attribute vec4 pos;
|
|
227
|
+
void main() {
|
|
228
|
+
gl_Position = pos;
|
|
229
|
+
}`;
|
|
230
|
+
// WebGL1 Fragment Shader
|
|
231
|
+
const fs = `
|
|
232
|
+
precision highp float; // highp float is an extension in WebGL1, but generally available
|
|
233
|
+
uniform float uSeed;
|
|
234
|
+
uniform int uIterations;
|
|
235
|
+
void main() {
|
|
236
|
+
float index = gl_FragCoord.x + (gl_FragCoord.y * 8.0);
|
|
237
|
+
float x = uSeed + (index * 0.015);
|
|
238
|
+
float r = 3.9999;
|
|
239
|
+
for(int i = 0; i < uIterations; i++) {
|
|
240
|
+
x = r * x * (1.0 - x);
|
|
241
|
+
}
|
|
242
|
+
gl_FragColor = vec4(x, 0.0, 0.0, 1.0); // Use gl_FragColor for WebGL1
|
|
243
|
+
}`;
|
|
244
|
+
|
|
245
|
+
const program = this._createProgram(gl, vs, fs);
|
|
246
|
+
gl.useProgram(program);
|
|
247
|
+
|
|
248
|
+
const posAttr = gl.getAttribLocation(program, 'pos');
|
|
249
|
+
const buffer = gl.createBuffer();
|
|
250
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
251
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW);
|
|
252
|
+
gl.enableVertexAttribArray(posAttr);
|
|
253
|
+
gl.vertexAttribPointer(posAttr, 2, gl.FLOAT, false, 0, 0);
|
|
254
|
+
|
|
255
|
+
gl.uniform1f(gl.getUniformLocation(program, 'uSeed'), seed);
|
|
256
|
+
gl.uniform1i(gl.getUniformLocation(program, 'uIterations'), iterations);
|
|
257
|
+
|
|
258
|
+
// Check for OES_texture_float and WEBGL_color_buffer_float extensions for float textures and readPixels
|
|
259
|
+
const floatTextureExt = gl.getExtension('OES_texture_float');
|
|
260
|
+
const floatColorBufferExt = gl.getExtension('WEBGL_color_buffer_float');
|
|
261
|
+
|
|
262
|
+
if (!floatTextureExt || !floatColorBufferExt) {
|
|
263
|
+
throw new Error('WebGL1 float texture/color buffer extensions not supported. Cannot read float pixels.');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Create a framebuffer to render to a float texture
|
|
267
|
+
const texture = gl.createTexture();
|
|
268
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
269
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, canvas.width, canvas.height, 0, gl.RGBA, gl.FLOAT, null);
|
|
270
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
271
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
272
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
273
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
274
|
+
|
|
275
|
+
const fb = gl.createFramebuffer();
|
|
276
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
|
|
277
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
278
|
+
|
|
279
|
+
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
280
|
+
if (status !== gl.FRAMEBUFFER_COMPLETE) {
|
|
281
|
+
throw new Error('WebGL1 framebuffer not complete: ' + status);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
285
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
286
|
+
|
|
287
|
+
const pixels = new Float32Array(canvas.width * canvas.height * 4);
|
|
288
|
+
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.FLOAT, pixels);
|
|
289
|
+
|
|
290
|
+
const result = [];
|
|
291
|
+
for (let i = 0; i < 64; i++) {
|
|
292
|
+
result.push(pixels[i * 4]); // Only take the R component
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return result.map(v => v.toFixed(6)).join(',');
|
|
296
|
+
}
|
|
297
|
+
|
|
203
298
|
static _createProgram(gl, vsSource, fsSource) {
|
|
204
299
|
const vs = gl.createShader(gl.VERTEX_SHADER);
|
|
205
300
|
gl.shaderSource(vs, vsSource);
|
package/src/js/library.js
CHANGED
|
@@ -521,8 +521,8 @@ Optimization.geneticAlgorithmMultiObjective = function (
|
|
|
521
521
|
const offspring = [];
|
|
522
522
|
for (let i = 0; i < populationSize; i++) {
|
|
523
523
|
// Sélection simple pour l'exemple
|
|
524
|
-
|
|
525
|
-
|
|
524
|
+
const parent1 = population[crypto.randomInt(0, population.length)];
|
|
525
|
+
const parent2 = population[crypto.randomInt(0, population.length)];
|
|
526
526
|
let childIndividual = crossover(parent1.individual, parent2.individual);
|
|
527
527
|
if (secureRandom() < mutationRate) {
|
|
528
528
|
childIndividual = mutate(childIndividual, currentConfig); // mutate doit maintenant utiliser currentConfig
|
|
@@ -749,7 +749,7 @@ Optimization.Operators.createTournamentSelection = (options = {}) => {
|
|
|
749
749
|
|
|
750
750
|
for (let i = 0; i < tournamentSize; i++) {
|
|
751
751
|
const individual =
|
|
752
|
-
population[
|
|
752
|
+
population[crypto.randomInt(0, population.length)];
|
|
753
753
|
if (!best || individual.fitness < best.fitness) {
|
|
754
754
|
best = individual;
|
|
755
755
|
}
|
|
@@ -757,7 +757,7 @@ Optimization.Operators.createTournamentSelection = (options = {}) => {
|
|
|
757
757
|
// Retourne le meilleur trouvé. Dans le pire des cas (tous les scores sont Infinity),
|
|
758
758
|
// on retourne le premier candidat sélectionné au lieu de null.
|
|
759
759
|
if (!best) {
|
|
760
|
-
return population[
|
|
760
|
+
return population[crypto.randomInt(0, population.length)];
|
|
761
761
|
}
|
|
762
762
|
return best;
|
|
763
763
|
};
|
|
@@ -911,9 +911,12 @@ Optimization.Operators.solveTSP = (cities, options = {}) => {
|
|
|
911
911
|
// Voisinage : génère un chemin voisin en inversant une sous-séquence (heuristique 2-opt).
|
|
912
912
|
const pathNeighbor = (path) => {
|
|
913
913
|
const newPath = [...path];
|
|
914
|
-
|
|
915
|
-
let
|
|
916
|
-
|
|
914
|
+
if (newPath.length <= 1) return newPath;
|
|
915
|
+
let i = crypto.randomInt(0, newPath.length);
|
|
916
|
+
let j = crypto.randomInt(0, newPath.length);
|
|
917
|
+
while (i === j) {
|
|
918
|
+
j = crypto.randomInt(0, newPath.length);
|
|
919
|
+
}
|
|
917
920
|
const [start, end] = [Math.min(i, j), Math.max(i, j)];
|
|
918
921
|
|
|
919
922
|
const segment = newPath.slice(start, end + 1).reverse();
|
|
@@ -971,7 +974,7 @@ Optimization.Operators.solvePortfolio = (
|
|
|
971
974
|
|
|
972
975
|
const mutate = (p) => {
|
|
973
976
|
const newP = [...p];
|
|
974
|
-
|
|
977
|
+
const i = crypto.randomInt(0, newP.length);
|
|
975
978
|
newP[i] += (secureRandom() - 0.5) * 0.2; // Mutation douce
|
|
976
979
|
newP[i] = Math.max(0, newP[i]); // Les poids ne peuvent être négatifs
|
|
977
980
|
return newP;
|
|
@@ -1105,7 +1108,7 @@ Optimization.Operators.solveFacilityLocation = (
|
|
|
1105
1108
|
// Voisinage : déplace légèrement une infrastructure au hasard.
|
|
1106
1109
|
const facilityNeighbor = (facilities) => {
|
|
1107
1110
|
const newFacilities = facilities.map((f) => ({ ...f }));
|
|
1108
|
-
|
|
1111
|
+
const i = crypto.randomInt(0, numFacilities);
|
|
1109
1112
|
const moveX = (secureRandom() - 0.5) * (bounds.maxX - bounds.minX) * 0.1;
|
|
1110
1113
|
const moveY = (secureRandom() - 0.5) * (bounds.maxY - bounds.minY) * 0.1;
|
|
1111
1114
|
|
|
@@ -1326,16 +1329,32 @@ Optimization.Operators.benfordTest = (numbers) => {
|
|
|
1326
1329
|
// Si l'entrée n'est pas un tableau, on ne peut pas l'analyser.
|
|
1327
1330
|
return 0;
|
|
1328
1331
|
}
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1332
|
+
const counts = Array(10).fill(0);
|
|
1333
|
+
let validCount = 0;
|
|
1331
1334
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1335
|
+
for (let i = 0; i < numbers.length; i++) {
|
|
1336
|
+
let n = numbers[i];
|
|
1337
|
+
if (typeof n === 'string') {
|
|
1338
|
+
n = parseFloat(n);
|
|
1339
|
+
}
|
|
1340
|
+
if (typeof n !== 'number' || isNaN(n) || !isFinite(n)) {
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
const val = Math.abs(n);
|
|
1344
|
+
if (val === 0) {
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
const log = Math.log10(val);
|
|
1348
|
+
const factor = Math.pow(10, Math.floor(log));
|
|
1349
|
+
let digit = Math.floor(val / factor);
|
|
1350
|
+
if (digit >= 1 && digit <= 9) {
|
|
1351
|
+
counts[digit]++;
|
|
1352
|
+
validCount++;
|
|
1353
|
+
}
|
|
1334
1354
|
}
|
|
1335
1355
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
counts[parseInt(leadingDigits[i], 10)]++;
|
|
1356
|
+
if (validCount < 10) {
|
|
1357
|
+
return 0; // Pas assez de données pour un test fiable
|
|
1339
1358
|
}
|
|
1340
1359
|
|
|
1341
1360
|
// Distribution attendue selon la loi de Benford pour le premier chiffre
|
|
@@ -1346,7 +1365,7 @@ Optimization.Operators.benfordTest = (numbers) => {
|
|
|
1346
1365
|
|
|
1347
1366
|
let totalDeviation = 0;
|
|
1348
1367
|
for (let i = 1; i <= 9; i++) {
|
|
1349
|
-
const observedFrequency = (counts[i] /
|
|
1368
|
+
const observedFrequency = (counts[i] / validCount) * 100;
|
|
1350
1369
|
const expectedFrequency = benfordDistribution[i];
|
|
1351
1370
|
totalDeviation += Math.pow(observedFrequency - expectedFrequency, 2);
|
|
1352
1371
|
}
|
|
@@ -1485,7 +1504,7 @@ Optimization.Operators.solveFraudDetection = (context, options = {}) => {
|
|
|
1485
1504
|
const minTimeToClick = 100 + secureRandom() * 4900; // entre 100ms et 5s
|
|
1486
1505
|
const maxClickVariance = 1 + secureRandom() * 9999; // entre 1 et 10000
|
|
1487
1506
|
const minMouseEntropy = secureRandom() * 0.5; // entre 0 et 0.5
|
|
1488
|
-
const minScrollEvents =
|
|
1507
|
+
const minScrollEvents = crypto.randomInt(0, 10); // entre 0 et 10
|
|
1489
1508
|
return [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents];
|
|
1490
1509
|
};
|
|
1491
1510
|
|
|
@@ -1502,7 +1521,7 @@ Optimization.Operators.solveFraudDetection = (context, options = {}) => {
|
|
|
1502
1521
|
// Mutation : légère variation aléatoire d'un des seuils
|
|
1503
1522
|
const mutate = (solution) => {
|
|
1504
1523
|
const newSolution = [...solution];
|
|
1505
|
-
const i =
|
|
1524
|
+
const i = crypto.randomInt(0, 4);
|
|
1506
1525
|
// Amplitudes de mutation différentes pour chaque seuil
|
|
1507
1526
|
const mutationFactors = [500, 1000, 0.1, 2];
|
|
1508
1527
|
const mutationFactor = mutationFactors[i];
|
|
@@ -1646,7 +1665,7 @@ Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
|
|
|
1646
1665
|
burstWeight: 20 + secureRandom() * 40,
|
|
1647
1666
|
scrapeThreshold: 500 + secureRandom() * 1000,
|
|
1648
1667
|
scrapeWeight: 15 + secureRandom() * 35,
|
|
1649
|
-
sequenceLength: 3 +
|
|
1668
|
+
sequenceLength: 3 + crypto.randomInt(0, 3),
|
|
1650
1669
|
sequenceWeight: 20 + secureRandom() * 50,
|
|
1651
1670
|
regularityThreshold: 50 + secureRandom() * 200,
|
|
1652
1671
|
regularityWeight: 20 + secureRandom() * 40,
|
|
@@ -1696,7 +1715,7 @@ Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
|
|
|
1696
1715
|
}
|
|
1697
1716
|
|
|
1698
1717
|
const keys = Object.keys(newConfig[sectionToMutate]);
|
|
1699
|
-
const keyToMutate = keys[
|
|
1718
|
+
const keyToMutate = keys[crypto.randomInt(0, keys.length)];
|
|
1700
1719
|
|
|
1701
1720
|
if (keyToMutate === 'honeypotScore') return newConfig; // Ne pas muter le poids du honeypot
|
|
1702
1721
|
|
package/src/js/mongodb-store.js
CHANGED
|
@@ -1,80 +1,166 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Creates a store adapter for MongoDB.
|
|
3
|
-
* This adapter uses a collection as a key-value store and leverages MongoDB's TTL indexes
|
|
4
|
-
* for automatic expiration of documents.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a store adapter for a MongoDB collection.
|
|
9
|
-
* It's recommended to pass the `db` object and let the adapter handle the collection.
|
|
10
|
-
*
|
|
11
|
-
* **Note:** For TTL to work, you must create a TTL index on the `expiresAt` field in your collection.
|
|
12
|
-
* In the mongo shell, run:
|
|
13
|
-
* `db.yourCollectionName.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })`
|
|
14
|
-
*
|
|
15
|
-
* @param {import('mongodb').Db} db - An instance of a MongoDB Db object.
|
|
16
|
-
* @param {string} [collectionName='fingerprint_store'] - The name of the collection to use.
|
|
17
|
-
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
18
|
-
*/
|
|
19
|
-
export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
|
|
20
|
-
const collection = db.collection(collectionName);
|
|
21
|
-
|
|
22
|
-
// Custom replacer/reviver to handle Set serialization (identical to Redis/SQL stores)
|
|
23
|
-
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
24
|
-
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file Creates a store adapter for MongoDB.
|
|
3
|
+
* This adapter uses a collection as a key-value store and leverages MongoDB's TTL indexes
|
|
4
|
+
* for automatic expiration of documents.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a store adapter for a MongoDB collection.
|
|
9
|
+
* It's recommended to pass the `db` object and let the adapter handle the collection.
|
|
10
|
+
*
|
|
11
|
+
* **Note:** For TTL to work, you must create a TTL index on the `expiresAt` field in your collection.
|
|
12
|
+
* In the mongo shell, run:
|
|
13
|
+
* `db.yourCollectionName.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })`
|
|
14
|
+
*
|
|
15
|
+
* @param {import('mongodb').Db} db - An instance of a MongoDB Db object.
|
|
16
|
+
* @param {string} [collectionName='fingerprint_store'] - The name of the collection to use.
|
|
17
|
+
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
18
|
+
*/
|
|
19
|
+
export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
|
|
20
|
+
const collection = db.collection(collectionName);
|
|
21
|
+
|
|
22
|
+
// Custom replacer/reviver to handle Set serialization (identical to Redis/SQL stores)
|
|
23
|
+
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
24
|
+
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
25
|
+
|
|
26
|
+
const localStore = new Map();
|
|
27
|
+
const localTimeouts = new Map();
|
|
28
|
+
let isDown = false;
|
|
29
|
+
let reconnecting = false;
|
|
30
|
+
|
|
31
|
+
const triggerFallback = () => {
|
|
32
|
+
if (!isDown) {
|
|
33
|
+
isDown = true;
|
|
34
|
+
attemptReconnection();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const attemptReconnection = () => {
|
|
39
|
+
if (reconnecting) return;
|
|
40
|
+
reconnecting = true;
|
|
41
|
+
const interval = setInterval(async () => {
|
|
42
|
+
try {
|
|
43
|
+
if (db.admin) {
|
|
44
|
+
await db.admin().ping();
|
|
45
|
+
} else {
|
|
46
|
+
await collection.findOne({}, { projection: { _id: 1 } });
|
|
47
|
+
}
|
|
48
|
+
isDown = false;
|
|
49
|
+
reconnecting = false;
|
|
50
|
+
clearInterval(interval);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// Toujours déconnecté
|
|
53
|
+
}
|
|
54
|
+
}, 5000);
|
|
55
|
+
if (interval.unref) interval.unref();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const setLocal = (key, value, ttl) => {
|
|
59
|
+
localStore.set(key, value);
|
|
60
|
+
if (localTimeouts.has(key)) {
|
|
61
|
+
clearTimeout(localTimeouts.get(key));
|
|
62
|
+
}
|
|
63
|
+
if (ttl && ttl > 0) {
|
|
64
|
+
const timeout = setTimeout(() => {
|
|
65
|
+
localStore.delete(key);
|
|
66
|
+
localTimeouts.delete(key);
|
|
67
|
+
}, ttl * 1000);
|
|
68
|
+
if (timeout.unref) timeout.unref();
|
|
69
|
+
localTimeouts.set(key, timeout);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
async get(key) {
|
|
75
|
+
if (isDown) {
|
|
76
|
+
return localStore.get(key) || null;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const doc = await collection.findOne({ _id: key });
|
|
80
|
+
if (!doc) return null;
|
|
81
|
+
|
|
82
|
+
// Active expiration check to bypass eventual consistency of MongoDB's 60s TTL cleanup daemon
|
|
83
|
+
if (doc.expiresAt && new Date(doc.expiresAt) < new Date()) {
|
|
84
|
+
await this.delete(key);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(doc.value, reviver);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// Fallback for legacy un-serialized raw values
|
|
92
|
+
return doc.value;
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
triggerFallback();
|
|
96
|
+
return localStore.get(key) || null;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
async set(key, value, ttl) {
|
|
100
|
+
if (isDown) {
|
|
101
|
+
setLocal(key, value, ttl);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const stringValue = JSON.stringify(value, replacer);
|
|
106
|
+
const doc = {
|
|
107
|
+
_id: key,
|
|
108
|
+
value: stringValue,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (ttl && ttl > 0) {
|
|
112
|
+
// Set the expiration date for the TTL index.
|
|
113
|
+
doc.expiresAt = new Date(Date.now() + ttl * 1000);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await collection.updateOne(
|
|
117
|
+
{ _id: key },
|
|
118
|
+
{ $set: doc },
|
|
119
|
+
{ upsert: true }
|
|
120
|
+
);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
triggerFallback();
|
|
123
|
+
setLocal(key, value, ttl);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
async has(key) {
|
|
127
|
+
if (isDown) {
|
|
128
|
+
return localStore.has(key);
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const doc = await collection.findOne({ _id: key }, { projection: { expiresAt: 1 } });
|
|
132
|
+
if (!doc) return false;
|
|
133
|
+
|
|
134
|
+
if (doc.expiresAt && new Date(doc.expiresAt) < new Date()) {
|
|
135
|
+
await this.delete(key);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
} catch (e) {
|
|
140
|
+
triggerFallback();
|
|
141
|
+
return localStore.has(key);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
async delete(key) {
|
|
145
|
+
if (localTimeouts.has(key)) {
|
|
146
|
+
clearTimeout(localTimeouts.get(key));
|
|
147
|
+
localTimeouts.delete(key);
|
|
148
|
+
}
|
|
149
|
+
localStore.delete(key);
|
|
150
|
+
if (isDown) return;
|
|
151
|
+
try {
|
|
152
|
+
await collection.deleteOne({ _id: key });
|
|
153
|
+
} catch (e) {
|
|
154
|
+
triggerFallback();
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
async init() {
|
|
158
|
+
try {
|
|
159
|
+
// Automates index configuration
|
|
160
|
+
await collection.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 });
|
|
161
|
+
} catch (e) {
|
|
162
|
+
triggerFallback();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
80
166
|
}
|