@anonympins/fingerprint 0.5.2 → 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 +18 -0
- package/package.json +1 -1
- package/src/js/fingerprint.client.js +1271 -1191
- package/src/js/fingerprint.js +202 -108
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +24 -8
- package/src/js/mongodb-store.js +165 -79
- package/src/js/pow.solver.inline.js +500 -14
- package/src/js/pow.solver.js +323 -14
- 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.test.js +32 -2
- package/src/js/tests/ip-reputation.test.js +1 -1
- package/src/js/tests/pow.solver.test.js +5 -2
- package/src/js/tests/usefulWorkSolver.test.js +162 -0
- package/src/php/Challenge/ChallengeUtils.php +21 -8
- package/src/php/FingerprintClient.php +22 -5
- package/src/php/FingerprintEngine.php +232 -56
- 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
package/src/js/pow.solver.js
CHANGED
|
@@ -269,15 +269,17 @@ export async function solveCpuTarget(message, target) {
|
|
|
269
269
|
*/
|
|
270
270
|
export async function solveMemory(seed, difficulty) {
|
|
271
271
|
const wasmModule = typeof window !== 'undefined' ? (window.wasmModule || (window.ClientLibrary && window.ClientLibrary.wasmModule)) : null;
|
|
272
|
+
let solution;
|
|
273
|
+
let useWasm = false;
|
|
272
274
|
if (wasmModule && typeof wasmModule._solve_memory_challenge === 'function') {
|
|
273
275
|
const encoder = new TextEncoder();
|
|
274
276
|
const seedBytes = encoder.encode(seed);
|
|
275
277
|
const ptr = wasmModule._malloc(seedBytes.length + 1);
|
|
276
278
|
wasmModule.HEAPU8.set(seedBytes, ptr);
|
|
277
279
|
wasmModule.HEAPU8[ptr + seedBytes.length] = 0; // Null-terminator
|
|
278
|
-
|
|
280
|
+
solution = wasmModule._solve_memory_challenge(ptr, difficulty);
|
|
279
281
|
wasmModule._free(ptr);
|
|
280
|
-
|
|
282
|
+
useWasm = true;
|
|
281
283
|
}
|
|
282
284
|
|
|
283
285
|
const size = difficulty * 1024 * 1024;
|
|
@@ -325,19 +327,21 @@ export async function solveMemory(seed, difficulty) {
|
|
|
325
327
|
}
|
|
326
328
|
const merkleRoot = tree[tree.length - 1][0];
|
|
327
329
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
if (!useWasm) {
|
|
331
|
+
function readBuffer(blocks, addr) {
|
|
332
|
+
const blockIdx = Math.floor(addr / 1024);
|
|
333
|
+
const elementIdx = addr % 1024;
|
|
334
|
+
return blocks[blockIdx][elementIdx];
|
|
335
|
+
}
|
|
333
336
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
337
|
+
const totalElements = numBlocks * 1024;
|
|
338
|
+
let addr = totalElements > 0 ? readBuffer(blocks, 0) % totalElements : 0;
|
|
339
|
+
solution = 0;
|
|
340
|
+
const iterations = 1024;
|
|
341
|
+
for (let i = 0; i < iterations; i++) {
|
|
342
|
+
addr = readBuffer(blocks, addr) % totalElements;
|
|
343
|
+
solution ^= addr;
|
|
344
|
+
}
|
|
341
345
|
}
|
|
342
346
|
|
|
343
347
|
const challengedIndices = [];
|
|
@@ -430,6 +434,81 @@ export async function solveTsp(cities, targetMaxDistance) {
|
|
|
430
434
|
|
|
431
435
|
// --- Fonctions d'optimisation copiées/adaptées de library.js pour le client ---
|
|
432
436
|
|
|
437
|
+
function paretoDominates(objectivesA, objectivesB) {
|
|
438
|
+
let aIsBetterInOne = false;
|
|
439
|
+
for (let i = 0; i < objectivesA.length; i++) {
|
|
440
|
+
if (objectivesA[i] > objectivesB[i]) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
if (objectivesA[i] < objectivesB[i]) {
|
|
444
|
+
aIsBetterInOne = true;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return aIsBetterInOne;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function nonDominatedSort(populationWithObjectives) {
|
|
451
|
+
const fronts = [[]];
|
|
452
|
+
for (const p1 of populationWithObjectives) {
|
|
453
|
+
p1.dominationCount = 0;
|
|
454
|
+
p1.dominatedSolutions = [];
|
|
455
|
+
for (const p2 of populationWithObjectives) {
|
|
456
|
+
if (p1 === p2) continue;
|
|
457
|
+
if (paretoDominates(p1.objectives, p2.objectives)) {
|
|
458
|
+
p1.dominatedSolutions.push(p2);
|
|
459
|
+
} else if (paretoDominates(p2.objectives, p1.objectives)) {
|
|
460
|
+
p1.dominationCount++;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (p1.dominationCount === 0) {
|
|
464
|
+
p1.rank = 0;
|
|
465
|
+
fronts[0].push(p1);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
let i = 0;
|
|
470
|
+
while (fronts[i] && fronts[i].length > 0) {
|
|
471
|
+
const nextFront = [];
|
|
472
|
+
for (const p1 of fronts[i]) {
|
|
473
|
+
for (const p2 of p1.dominatedSolutions) {
|
|
474
|
+
p2.dominationCount--;
|
|
475
|
+
if (p2.dominationCount === 0) {
|
|
476
|
+
p2.rank = i + 1;
|
|
477
|
+
nextFront.push(p2);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
i++;
|
|
482
|
+
if (nextFront.length > 0) {
|
|
483
|
+
fronts[i] = nextFront;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return fronts;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function calculateCrowdingDistance(front) {
|
|
490
|
+
if (front.length === 0) return;
|
|
491
|
+
front.forEach((p) => (p.crowdingDistance = 0));
|
|
492
|
+
const numObjectives = front[0].objectives.length;
|
|
493
|
+
|
|
494
|
+
for (let i = 0; i < numObjectives; i++) {
|
|
495
|
+
front.sort((a, b) => a.objectives[i] - b.objectives[i]);
|
|
496
|
+
const minObj = front[0].objectives[i];
|
|
497
|
+
const maxObj = front[front.length - 1].objectives[i];
|
|
498
|
+
|
|
499
|
+
front[0].crowdingDistance = Infinity;
|
|
500
|
+
front[front.length - 1].crowdingDistance = Infinity;
|
|
501
|
+
|
|
502
|
+
if (maxObj === minObj) continue;
|
|
503
|
+
|
|
504
|
+
for (let j = 1; j < front.length - 1; j++) {
|
|
505
|
+
front[j].crowdingDistance +=
|
|
506
|
+
(front[j + 1].objectives[i] - front[j - 1].objectives[i]) /
|
|
507
|
+
(maxObj - minObj);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
433
512
|
const ClientOptimizers = {
|
|
434
513
|
simulatedAnnealing(initialSolution, evaluator, neighbor, iterations, temp, cooling) {
|
|
435
514
|
let currentSolution = initialSolution;
|
|
@@ -467,6 +546,236 @@ const ClientOptimizers = {
|
|
|
467
546
|
population = newPopulation;
|
|
468
547
|
}
|
|
469
548
|
return population;
|
|
549
|
+
},
|
|
550
|
+
|
|
551
|
+
geneticAlgorithmMultiObjective(createIndividual, fitnessFunction, crossover, mutate, options = {}) {
|
|
552
|
+
const generations = options.generations || 50;
|
|
553
|
+
const populationSize = options.populationSize || 40;
|
|
554
|
+
const mutationRate = options.mutationRate !== undefined ? options.mutationRate : 0.1;
|
|
555
|
+
|
|
556
|
+
let population = Array.from({ length: populationSize }, () => ({
|
|
557
|
+
individual: createIndividual(),
|
|
558
|
+
}));
|
|
559
|
+
population.forEach((p) => (p.objectives = fitnessFunction(p.individual)));
|
|
560
|
+
|
|
561
|
+
for (let gen = 0; gen < generations; gen++) {
|
|
562
|
+
const offspring = [];
|
|
563
|
+
for (let i = 0; i < populationSize; i++) {
|
|
564
|
+
const parent1 = population[Math.floor(Math.random() * population.length)];
|
|
565
|
+
const parent2 = population[Math.floor(Math.random() * population.length)];
|
|
566
|
+
let childIndividual = crossover(parent1.individual, parent2.individual);
|
|
567
|
+
if (Math.random() < mutationRate) {
|
|
568
|
+
childIndividual = mutate(childIndividual);
|
|
569
|
+
}
|
|
570
|
+
const child = { individual: childIndividual };
|
|
571
|
+
child.objectives = fitnessFunction(child.individual);
|
|
572
|
+
offspring.push(child);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const combinedPopulation = [...population, ...offspring];
|
|
576
|
+
const fronts = nonDominatedSort(combinedPopulation);
|
|
577
|
+
const newPopulation = [];
|
|
578
|
+
for (const front of fronts) {
|
|
579
|
+
if (newPopulation.length + front.length <= populationSize) {
|
|
580
|
+
newPopulation.push(...front);
|
|
581
|
+
} else {
|
|
582
|
+
calculateCrowdingDistance(front);
|
|
583
|
+
front.sort((a, b) => b.crowdingDistance - a.crowdingDistance);
|
|
584
|
+
const remaining = populationSize - newPopulation.length;
|
|
585
|
+
newPopulation.push(...front.slice(0, remaining));
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
population = newPopulation;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const finalFronts = nonDominatedSort(population);
|
|
593
|
+
const bestFront = finalFronts.length > 0 ? finalFronts[0] : [];
|
|
594
|
+
const uniqueSolutionsMap = new Map();
|
|
595
|
+
for (const p of bestFront) {
|
|
596
|
+
const key = JSON.stringify(p.objectives);
|
|
597
|
+
if (!uniqueSolutionsMap.has(key)) {
|
|
598
|
+
uniqueSolutionsMap.set(key, {
|
|
599
|
+
solution: p.individual,
|
|
600
|
+
objectives: p.objectives,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return Array.from(uniqueSolutionsMap.values());
|
|
605
|
+
},
|
|
606
|
+
|
|
607
|
+
'cpc.solve'(context, options = {}) {
|
|
608
|
+
const optimalBaseCommission = context.platformParams ? context.platformParams.optimalBaseCommission : 0.3;
|
|
609
|
+
const optimalBonusFactor = context.platformParams ? context.platformParams.optimalBonusFactor : 0.1;
|
|
610
|
+
const websiteQualityScore = ((context.website && context.website.relevanceScore) || 50) / 100;
|
|
611
|
+
const effectiveCommissionRate = Math.max(0, optimalBaseCommission - websiteQualityScore * optimalBonusFactor);
|
|
612
|
+
|
|
613
|
+
const advertiserDemand = (cpc) => {
|
|
614
|
+
if (cpc <= 0) return Infinity;
|
|
615
|
+
return (context.advertiser.credits || 0) / cpc;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const supply = context.estimatedImpressions || 1;
|
|
619
|
+
const competingAdsCount = context.competingAds ? context.competingAds.length : 0;
|
|
620
|
+
const competitionFactor = Math.min(2.5, 1 + competingAdsCount * 0.1);
|
|
621
|
+
|
|
622
|
+
const fitnessFunction = (cpcMultiplier) => {
|
|
623
|
+
const adjustedCPC = 1.0 * cpcMultiplier * competitionFactor;
|
|
624
|
+
if (adjustedCPC < 0.1) return [Infinity, Infinity, Infinity];
|
|
625
|
+
|
|
626
|
+
const demand = advertiserDemand(adjustedCPC);
|
|
627
|
+
const estimatedClicks = Math.min(demand, supply);
|
|
628
|
+
const platformRevenue = estimatedClicks * adjustedCPC * effectiveCommissionRate;
|
|
629
|
+
const advertiserValue = estimatedClicks;
|
|
630
|
+
const marketImbalance = Math.abs(demand - supply);
|
|
631
|
+
|
|
632
|
+
return [-platformRevenue, -advertiserValue, marketImbalance];
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const createIndividual = () => 0.5 + Math.random() * 4.5;
|
|
636
|
+
const crossover = (cpc1, cpc2) => (cpc1 + cpc2) / 2;
|
|
637
|
+
const mutate = (cpc) => Math.max(0.1, cpc + (Math.random() - 0.5) * 0.5);
|
|
638
|
+
|
|
639
|
+
const gaOptions = {
|
|
640
|
+
generations: 50,
|
|
641
|
+
populationSize: 40,
|
|
642
|
+
...options,
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const paretoFront = ClientOptimizers.geneticAlgorithmMultiObjective(
|
|
646
|
+
createIndividual,
|
|
647
|
+
fitnessFunction,
|
|
648
|
+
crossover,
|
|
649
|
+
mutate,
|
|
650
|
+
gaOptions
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
paretoFront: paretoFront.map((result) => {
|
|
655
|
+
const cpcMultiplier = result.solution;
|
|
656
|
+
const finalCpc = Math.max(0.1, 1.0 * cpcMultiplier * competitionFactor);
|
|
657
|
+
return {
|
|
658
|
+
...result,
|
|
659
|
+
solution: finalCpc,
|
|
660
|
+
};
|
|
661
|
+
})
|
|
662
|
+
};
|
|
663
|
+
},
|
|
664
|
+
|
|
665
|
+
'fraud.solve'(context, options = {}) {
|
|
666
|
+
const legitimateClicks = context.legitimateClicks || [];
|
|
667
|
+
const fraudulentClicks = context.fraudulentClicks || [];
|
|
668
|
+
|
|
669
|
+
const fitnessFunction = (solution) => {
|
|
670
|
+
const [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents] = solution;
|
|
671
|
+
if (
|
|
672
|
+
minTimeToClick < 100 ||
|
|
673
|
+
minTimeToClick > 5000 ||
|
|
674
|
+
maxClickVariance < 1 ||
|
|
675
|
+
maxClickVariance > 10000 ||
|
|
676
|
+
minMouseEntropy < 0 ||
|
|
677
|
+
minMouseEntropy > 1 ||
|
|
678
|
+
minScrollEvents < 0
|
|
679
|
+
) {
|
|
680
|
+
return [Infinity, Infinity];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const calculateClickVariance = (clicks) => {
|
|
684
|
+
if (!clicks || clicks.length < 2) return 0;
|
|
685
|
+
const meanX = clicks.reduce((sum, c) => sum + c.clickX, 0) / clicks.length;
|
|
686
|
+
const meanY = clicks.reduce((sum, c) => sum + c.clickY, 0) / clicks.length;
|
|
687
|
+
return clicks.reduce((sum, c) => sum + Math.pow(c.clickX - meanX, 2) + Math.pow(c.clickY - meanY, 2), 0) / clicks.length;
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
const getClicksByFingerprint = (clickData) => {
|
|
691
|
+
const grouped = {};
|
|
692
|
+
for (const click of clickData) {
|
|
693
|
+
if (!grouped[click.fingerprint]) grouped[click.fingerprint] = [];
|
|
694
|
+
grouped[click.fingerprint].push(click);
|
|
695
|
+
}
|
|
696
|
+
return grouped;
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const legitimateGroups = getClicksByFingerprint(legitimateClicks);
|
|
700
|
+
const fraudulentGroups = getClicksByFingerprint(fraudulentClicks);
|
|
701
|
+
|
|
702
|
+
let truePositives = 0;
|
|
703
|
+
let falsePositives = 0;
|
|
704
|
+
|
|
705
|
+
for (const fingerprint in fraudulentGroups) {
|
|
706
|
+
const clicks = fraudulentGroups[fingerprint];
|
|
707
|
+
if (!clicks) continue;
|
|
708
|
+
const variance = calculateClickVariance(clicks);
|
|
709
|
+
const isTooFast = clicks.some((c) => c.timeToClick < minTimeToClick);
|
|
710
|
+
const isTooUniform = variance < maxClickVariance;
|
|
711
|
+
const hasLowEntropy = clicks.some((c) => c.mouseEntropy < minMouseEntropy);
|
|
712
|
+
const hasFewScrolls = clicks.some((c) => c.scrollEvents < minScrollEvents);
|
|
713
|
+
|
|
714
|
+
if (isTooFast || isTooUniform || hasLowEntropy || hasFewScrolls) {
|
|
715
|
+
truePositives++;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
for (const fingerprint in legitimateGroups) {
|
|
720
|
+
const clicks = legitimateGroups[fingerprint];
|
|
721
|
+
if (!clicks) continue;
|
|
722
|
+
const variance = calculateClickVariance(clicks);
|
|
723
|
+
if (
|
|
724
|
+
clicks.some((c) => c.timeToClick < minTimeToClick || c.mouseEntropy < minMouseEntropy || c.scrollEvents < minScrollEvents) ||
|
|
725
|
+
variance < maxClickVariance
|
|
726
|
+
) {
|
|
727
|
+
falsePositives++;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const totalFraudulent = Object.keys(fraudulentGroups).length || 1;
|
|
732
|
+
const totalLegitimate = Object.keys(legitimateGroups).length || 1;
|
|
733
|
+
|
|
734
|
+
const objective1 = 1 - truePositives / totalFraudulent;
|
|
735
|
+
const objective2 = falsePositives / totalLegitimate;
|
|
736
|
+
|
|
737
|
+
return [objective1, objective2];
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
const createIndividual = () => [
|
|
741
|
+
100 + Math.random() * 4900,
|
|
742
|
+
1 + Math.random() * 9999,
|
|
743
|
+
Math.random() * 0.5,
|
|
744
|
+
Math.floor(Math.random() * 10)
|
|
745
|
+
];
|
|
746
|
+
|
|
747
|
+
const crossover = (s1, s2) => [
|
|
748
|
+
(s1[0] + s2[0]) / 2,
|
|
749
|
+
(s1[1] + s2[1]) / 2,
|
|
750
|
+
(s1[2] + s2[2]) / 2,
|
|
751
|
+
(s1[3] + s2[3]) / 2,
|
|
752
|
+
];
|
|
753
|
+
|
|
754
|
+
const mutate = (solution) => {
|
|
755
|
+
const newSolution = [...solution];
|
|
756
|
+
const i = Math.floor(Math.random() * 4);
|
|
757
|
+
const mutationFactors = [500, 1000, 0.1, 2];
|
|
758
|
+
newSolution[i] += (Math.random() - 0.5) * mutationFactors[i];
|
|
759
|
+
return newSolution;
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
const gaOptions = {
|
|
763
|
+
generations: 80,
|
|
764
|
+
populationSize: 60,
|
|
765
|
+
...options,
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
const paretoFront = ClientOptimizers.geneticAlgorithmMultiObjective(
|
|
769
|
+
createIndividual,
|
|
770
|
+
fitnessFunction,
|
|
771
|
+
crossover,
|
|
772
|
+
mutate,
|
|
773
|
+
gaOptions
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
paretoFront
|
|
778
|
+
};
|
|
470
779
|
}
|
|
471
780
|
};
|
|
472
781
|
|
|
@@ -586,7 +586,7 @@ let managerPromise = null;
|
|
|
586
586
|
* @returns {Promise<ProblemManager>} The singleton instance.
|
|
587
587
|
*/
|
|
588
588
|
export function getProblemManager(options = {}, store) {
|
|
589
|
-
const defaultPath = join(__dirname, '..', '..', 'problems.config.json');
|
|
589
|
+
const defaultPath = join(__dirname, '..', '..', 'config', 'problems.config.json');
|
|
590
590
|
const { configPath = defaultPath, config } = options;
|
|
591
591
|
|
|
592
592
|
const hasConfigChanged = problemManagerInstance && (
|
package/src/js/redis-store.js
CHANGED
|
@@ -1,43 +1,115 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Creates a store adapter for ioredis.
|
|
3
|
-
* This adapter handles the serialization and deserialization of complex objects,
|
|
4
|
-
* including the conversion of Set objects to arrays for storage in Redis.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a store adapter for an ioredis client.
|
|
9
|
-
* @param {import('ioredis').Redis} redisClient - An instance of the ioredis client.
|
|
10
|
-
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
11
|
-
*/
|
|
12
|
-
export function createRedisStore(redisClient) {
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @file Creates a store adapter for ioredis.
|
|
3
|
+
* This adapter handles the serialization and deserialization of complex objects,
|
|
4
|
+
* including the conversion of Set objects to arrays for storage in Redis.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a store adapter for an ioredis client.
|
|
9
|
+
* @param {import('ioredis').Redis} redisClient - An instance of the ioredis client.
|
|
10
|
+
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
11
|
+
*/
|
|
12
|
+
export function createRedisStore(redisClient) {
|
|
13
|
+
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
14
|
+
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
15
|
+
|
|
16
|
+
const localStore = new Map();
|
|
17
|
+
const localTimeouts = new Map();
|
|
18
|
+
let isDown = false;
|
|
19
|
+
let reconnecting = false;
|
|
20
|
+
|
|
21
|
+
const triggerFallback = () => {
|
|
22
|
+
if (!isDown) {
|
|
23
|
+
isDown = true;
|
|
24
|
+
attemptReconnection();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const attemptReconnection = () => {
|
|
29
|
+
if (reconnecting) return;
|
|
30
|
+
reconnecting = true;
|
|
31
|
+
const interval = setInterval(async () => {
|
|
32
|
+
try {
|
|
33
|
+
await redisClient.ping();
|
|
34
|
+
isDown = false;
|
|
35
|
+
reconnecting = false;
|
|
36
|
+
clearInterval(interval);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// Continue de tenter la reconnexion toutes les 5 secondes
|
|
39
|
+
}
|
|
40
|
+
}, 5000);
|
|
41
|
+
if (interval.unref) interval.unref();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const setLocal = (key, value, ttl) => {
|
|
45
|
+
localStore.set(key, value);
|
|
46
|
+
if (localTimeouts.has(key)) {
|
|
47
|
+
clearTimeout(localTimeouts.get(key));
|
|
48
|
+
}
|
|
49
|
+
if (ttl && ttl > 0) {
|
|
50
|
+
const timeout = setTimeout(() => {
|
|
51
|
+
localStore.delete(key);
|
|
52
|
+
localTimeouts.delete(key);
|
|
53
|
+
}, ttl * 1000);
|
|
54
|
+
if (timeout.unref) timeout.unref();
|
|
55
|
+
localTimeouts.set(key, timeout);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
async get(key) {
|
|
61
|
+
if (isDown) {
|
|
62
|
+
return localStore.get(key) || null;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const value = await redisClient.get(key);
|
|
66
|
+
if (!value) return null;
|
|
67
|
+
return JSON.parse(value, reviver);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
triggerFallback();
|
|
70
|
+
return localStore.get(key) || null;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
async set(key, value, ttl) {
|
|
74
|
+
if (isDown) {
|
|
75
|
+
setLocal(key, value, ttl);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const stringValue = JSON.stringify(value, replacer);
|
|
80
|
+
if (ttl && ttl > 0) {
|
|
81
|
+
await redisClient.set(key, stringValue, 'EX', ttl);
|
|
82
|
+
} else {
|
|
83
|
+
await redisClient.set(key, stringValue);
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
triggerFallback();
|
|
87
|
+
setLocal(key, value, ttl);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async has(key) {
|
|
91
|
+
if (isDown) {
|
|
92
|
+
return localStore.has(key);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return (await redisClient.exists(key)) === 1;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
triggerFallback();
|
|
98
|
+
return localStore.has(key);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
async delete(key) {
|
|
102
|
+
if (localTimeouts.has(key)) {
|
|
103
|
+
clearTimeout(localTimeouts.get(key));
|
|
104
|
+
localTimeouts.delete(key);
|
|
105
|
+
}
|
|
106
|
+
localStore.delete(key);
|
|
107
|
+
if (isDown) return;
|
|
108
|
+
try {
|
|
109
|
+
await redisClient.del(key);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
triggerFallback();
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
43
115
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
2
|
+
import { __internal } from '../fingerprint.js';
|
|
3
|
+
|
|
4
|
+
const { dnsCircuitBreaker, recordDnsSuccess, recordDnsFailure, canAttemptDns } = __internal;
|
|
5
|
+
|
|
6
|
+
describe('DNS Circuit Breaker', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
dnsCircuitBreaker.state = 'CLOSED';
|
|
9
|
+
dnsCircuitBreaker.failureCount = 0;
|
|
10
|
+
dnsCircuitBreaker.lastStateChange = 0;
|
|
11
|
+
dnsCircuitBreaker.threshold = 5;
|
|
12
|
+
dnsCircuitBreaker.cooldownMs = 1000; // 1s de cooldown pour accélérer le test unitaire
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('devrait démarrer à l\'état CLOSED et autoriser les résolutions', () => {
|
|
16
|
+
expect(canAttemptDns()).toBe(true);
|
|
17
|
+
expect(dnsCircuitBreaker.state).toBe('CLOSED');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('devrait s\'ouvrir après avoir atteint le seuil d\'échecs (5)', () => {
|
|
21
|
+
for (let i = 0; i < 4; i++) {
|
|
22
|
+
recordDnsFailure();
|
|
23
|
+
expect(canAttemptDns()).toBe(true);
|
|
24
|
+
expect(dnsCircuitBreaker.state).toBe('CLOSED');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
recordDnsFailure();
|
|
28
|
+
expect(canAttemptDns()).toBe(false);
|
|
29
|
+
expect(dnsCircuitBreaker.state).toBe('OPEN');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('devrait passer en HALF-OPEN après expiration du cooldown', async () => {
|
|
33
|
+
for (let i = 0; i < 5; i++) {
|
|
34
|
+
recordDnsFailure();
|
|
35
|
+
}
|
|
36
|
+
expect(canAttemptDns()).toBe(false);
|
|
37
|
+
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
39
|
+
|
|
40
|
+
expect(canAttemptDns()).toBe(true);
|
|
41
|
+
expect(dnsCircuitBreaker.state).toBe('HALF-OPEN');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('devrait se refermer (CLOSED) après un succès en HALF-OPEN', async () => {
|
|
45
|
+
for (let i = 0; i < 5; i++) {
|
|
46
|
+
recordDnsFailure();
|
|
47
|
+
}
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
49
|
+
expect(canAttemptDns()).toBe(true); // Passage en HALF-OPEN
|
|
50
|
+
|
|
51
|
+
recordDnsSuccess();
|
|
52
|
+
expect(dnsCircuitBreaker.state).toBe('CLOSED');
|
|
53
|
+
expect(dnsCircuitBreaker.failureCount).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
});
|