@anonympins/fingerprint 0.3.2 → 0.3.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/README.md +1080 -834
  3. package/composer.json +39 -0
  4. package/index.js +5 -0
  5. package/package.json +31 -23
  6. package/phpunit.xml +21 -0
  7. package/public/fp.js +2 -0
  8. package/src/js/build-client.js +69 -0
  9. package/{fingerprint.builder.js → src/js/fingerprint.builder.js} +168 -175
  10. package/{fingerprint.client.js → src/js/fingerprint.client.js} +634 -538
  11. package/src/js/fingerprint.client.obfuscated.js +1 -0
  12. package/{fingerprint.js → src/js/fingerprint.js} +3733 -3294
  13. package/{library.js → src/js/library.js} +1729 -1729
  14. package/{problem-manager.js → src/js/problem-manager.js} +539 -522
  15. package/src/php/AutoTuner.php +155 -0
  16. package/src/php/Challenge/ChallengeUtils.php +306 -0
  17. package/src/php/Config/SecurityProfiles.php +267 -0
  18. package/src/php/DirectFingerprint.php +81 -0
  19. package/src/php/FingerprintBuilder.php +186 -0
  20. package/src/php/FingerprintClient.php +132 -0
  21. package/src/php/FingerprintEngine.php +863 -0
  22. package/src/php/Optimization/FunctionRegistry.php +63 -0
  23. package/src/php/Optimization/Optimization.php +256 -0
  24. package/src/php/Optimization/OptimizationOperators.php +305 -0
  25. package/src/php/Optimization/ProblemInitializers.php +53 -0
  26. package/src/php/ProblemManager.php +255 -0
  27. package/src/php/RequestContext.php +91 -0
  28. package/src/php/Store/IStore.php +42 -0
  29. package/src/php/Store/InMemoryStore.php +67 -0
  30. package/src/php/Store/StoreManager.php +36 -0
  31. package/src/php/Tests/ChallengeUtilsTest.php +82 -0
  32. package/src/php/Tests/FingerprintBuilderTest.php +58 -0
  33. package/src/php/Tests/FingerprintEngineTest.php +300 -0
  34. package/src/php/Tests/IpReputationTest.php +157 -0
  35. package/src/php/Tests/PowTest.php +40 -0
  36. package/src/php/Tests/ProblemManagerTest.php +295 -0
  37. package/src/php/Tests/RequestUtilsTest.php +81 -0
  38. package/src/php/Tests/problems.config.json +9 -0
  39. package/src/php/Utils/BigInt.php +145 -0
  40. package/src/php/Utils/BlockList.php +100 -0
  41. package/src/php/Utils/Logger.php +30 -0
  42. package/src/php/Utils/MaliciousPatterns.php +59 -0
  43. package/src/php/Utils/RequestUtils.php +962 -0
  44. package/fingerprint.client.obfuscated.js +0 -1
  45. /package/{mongodb-store.js → src/js/mongodb-store.js} +0 -0
  46. /package/{optimization.worker.js → src/js/optimization.worker.js} +0 -0
  47. /package/{pow.solver.inline.js → src/js/pow.solver.inline.js} +0 -0
  48. /package/{pow.solver.js → src/js/pow.solver.js} +0 -0
  49. /package/{pow.worker.js → src/js/pow.worker.js} +0 -0
  50. /package/{redis-store.js → src/js/redis-store.js} +0 -0
  51. /package/{sql-store.js → src/js/sql-store.js} +0 -0
@@ -0,0 +1,295 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use PHPUnit\Framework\TestCase;
8
+ use Anonympins\Fingerprint\ProblemManager;
9
+ use Anonympins\Fingerprint\Store\IStore;
10
+ use Anonympins\Fingerprint\Store\InMemoryStore;
11
+ use Anonympins\Fingerprint\Optimization\FunctionRegistry; // Assurez-vous que cette classe est autoloadable
12
+
13
+ class ProblemManagerTest extends TestCase
14
+ {
15
+ private ?string $configPath = null;
16
+
17
+ protected function setUp(): void
18
+ {
19
+ // Réinitialise le singleton avant chaque test pour garantir l'isolation
20
+ ProblemManager::__internal_resetInstance();
21
+ FunctionRegistry::__internal_resetRegistry(); // Assure un registre de fonctions propre
22
+ $this->configPath = dirname(__FILE__).'/problems.config.json';
23
+ }
24
+
25
+ protected function tearDown(): void
26
+ {
27
+ ProblemManager::__internal_resetInstance();
28
+ FunctionRegistry::__internal_resetRegistry();
29
+ }
30
+
31
+ private function createConfigFile(array $content): void
32
+ {
33
+ // S'assurer que le répertoire existe
34
+ if (!is_dir(dirname($this->configPath))) {
35
+ mkdir(dirname($this->configPath), 0777, true);
36
+ }
37
+ file_put_contents($this->configPath, json_encode($content, JSON_PRETTY_PRINT));
38
+ }
39
+
40
+ public function testGetInstanceThrowsExceptionIfNotInitialized(): void
41
+ {
42
+ $this->expectException(\RuntimeException::class);
43
+ $this->expectExceptionMessage('ProblemManager must be initialized with configPath and store.');
44
+ ProblemManager::getInstance();
45
+ }
46
+
47
+ public function testGetInstanceCreatesAndReturnsInstance(): void
48
+ {
49
+ $storeMock = $this->createMock(IStore::class);
50
+
51
+ $instance1 = ProblemManager::getInstance($this->configPath, $storeMock);
52
+ $this->assertInstanceOf(ProblemManager::class, $instance1);
53
+
54
+ // Les appels suivants doivent retourner la même instance
55
+ $instance2 = ProblemManager::getInstance();
56
+ $this->assertSame($instance1, $instance2);
57
+ $this->assertTrue(ProblemManager::isInitialized());
58
+ }
59
+
60
+ public function testLoadProblemsHandlesMissingFile(): void
61
+ {
62
+ $storeMock = $this->createMock(IStore::class);
63
+ $missingConfigPath = dirname(__FILE__).'/non_existent_config.json';
64
+ if (file_exists($missingConfigPath)) {
65
+ unlink($missingConfigPath); // S'assurer que le fichier n'existe pas
66
+ }
67
+
68
+ // Ne doit pas lever d'exception, mais simplement ne pas initialiser de problèmes
69
+ $manager = ProblemManager::getInstance($missingConfigPath, $storeMock);
70
+ $this->assertNull($manager->dispatchWork(0.5));
71
+ }
72
+
73
+ public function testDispatchWorkCyclesThroughProblems(): void
74
+ {
75
+ $storeMock = $this->createMock(IStore::class);
76
+ $storeMock->method('get')->willReturn(null);
77
+
78
+ // Créer un fichier de configuration avec plusieurs problèmes pour tester le cycle
79
+ $this->createConfigFile([
80
+ [
81
+ "id" => "problem-1",
82
+ "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000, "scalingFactor" => 2.0]
83
+ ],
84
+ [
85
+ "id" => "problem-2",
86
+ "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 20000]
87
+ ],
88
+ [
89
+ "id" => "scaling-problem",
90
+ "workUnit" => ["type" => "multi_objective_genetic_algorithm", "baseGenerations" => 100, "scalingFactor" => 2.0, "solverName" => "test.solver"]
91
+ ],
92
+ [
93
+ "id" => "pareto-challenge",
94
+ "workUnit" => ["type" => "multi_objective_genetic_algorithm", "solverName" => "test.solver"]
95
+ ],
96
+ [
97
+ "id" => "problem-1",
98
+ "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000, "scalingFactor" => 2.0]
99
+ ]
100
+ ]);
101
+
102
+ $manager = ProblemManager::getInstance($this->configPath, $storeMock);
103
+
104
+ $work1 = $manager->dispatchWork(0.5);
105
+ $this->assertEquals('problem-1', $work1['problemId']);
106
+
107
+ $work2 = $manager->dispatchWork(0.5);
108
+ $this->assertEquals('problem-2', $work2['problemId']);
109
+
110
+ // Cycle through a few more times to ensure it loops
111
+ $work3 = $manager->dispatchWork(0.5);
112
+ $this->assertEquals('scaling-problem', $work3['problemId']);
113
+
114
+ $work4 = $manager->dispatchWork(0.5);
115
+ $this->assertEquals('pareto-challenge', $work4['problemId']);
116
+
117
+ $work5 = $manager->dispatchWork(0.5);
118
+ $this->assertEquals('problem-1', $work5['problemId']); // Back to the start
119
+ }
120
+
121
+ public function testDispatchWorkScalesDifficulty(): void
122
+ {
123
+ $storeMock = $this->createMock(IStore::class);
124
+ $storeMock->method('get')->willReturn(null);
125
+
126
+ $this->createConfigFile([
127
+ [
128
+ "id" => "problem-1",
129
+ "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000, "scalingFactor" => 2.0]
130
+ ],
131
+ [
132
+ "id" => "problem-1",
133
+ "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000, "scalingFactor" => 2.0]
134
+ ],
135
+ [
136
+ "id" => "scaling-problem",
137
+ "workUnit" => ["type" => "multi_objective_genetic_algorithm", "baseGenerations" => 100, "scalingFactor" => 2.0, "solverName" => "test.solver"]
138
+ ]
139
+ ]);
140
+
141
+ $manager = ProblemManager::getInstance($this->configPath, $storeMock);
142
+
143
+ // Facteur de suspicion faible
144
+ $workLow = $manager->dispatchWork(0.1);
145
+ $this->assertLessThan(12000, $workLow['task']['iterations']); // 10000 * 2^0.1 ≈ 10717
146
+
147
+ // Facteur de suspicion moyen - dispatch 'problem-1'
148
+ $workMedium = $manager->dispatchWork(0.5);
149
+ $this->assertEquals(14142, $workMedium['task']['iterations']); // 10000 * 2^0.5
150
+
151
+ // Facteur de suspicion élevé - dispatch 'scaling-problem'
152
+ $workHigh = $manager->dispatchWork(1.0);
153
+ $this->assertEquals(200, $workHigh['task']['generations']); // 100 * 2^1.0
154
+ }
155
+
156
+ public function testIntegrateSolutionUpdatesStateForBetterSolution(): void
157
+ {
158
+ $problemId = 'problem-1';
159
+
160
+ $storeMock = $this->createMock(IStore::class);
161
+
162
+ $this->createConfigFile([
163
+ [
164
+ "id" => $problemId,
165
+ "workUnit" => [
166
+ "type" => "simulated_annealing_iterations", // @phpstan-ignore-line
167
+ "baseIterations" => 10000,
168
+ // **LA CORRECTION** : La fonction de score est requise pour la vérification.
169
+ "scoreFunction" => "test.calculateEnergy"
170
+ ]
171
+ ]
172
+ ]);
173
+
174
+ // Enregistrer une fonction de score factice pour le test.
175
+ FunctionRegistry::register('test.calculateEnergy', function ($solution, $payload) { // @phpstan-ignore-line
176
+ return 800.0; // Retourne la nouvelle "meilleure" énergie attendue.
177
+ });
178
+
179
+ $storeMock->method('get')
180
+ ->with("problem-state:{$problemId}")
181
+ ->willReturn(['bestEnergy' => 1000.0]);
182
+
183
+ // On s'attend à ce que le store soit mis à jour avec la nouvelle meilleure solution
184
+ $storeMock->expects($this->once())
185
+ ->method('set')
186
+ ->with(
187
+ "problem-state:{$problemId}",
188
+ $this->callback(function ($state) {
189
+ return isset($state['bestEnergy']) && $state['bestEnergy'] === 800.0 && isset($state['bestSolution']);
190
+ })
191
+ );
192
+
193
+ $manager = ProblemManager::getInstance($this->configPath, $storeMock);
194
+
195
+ $newSolution = [
196
+ 'solution' => [1, 2, 3],
197
+ 'energy' => 800.0 // C'est une meilleure solution (score plus bas)
198
+ ];
199
+ $manager->integrateSolution($problemId, $newSolution);
200
+ }
201
+
202
+ public function testIntegrateSolutionDoesNotUpdateStateForWorseSolution(): void
203
+ {
204
+ $problemId = 'problem-1';
205
+
206
+ $storeMock = $this->createMock(IStore::class);
207
+
208
+ $this->createConfigFile([
209
+ [
210
+ "id" => $problemId,
211
+ "workUnit" => [
212
+ "type" => "simulated_annealing_iterations", // @phpstan-ignore-line
213
+ "baseIterations" => 10000,
214
+ "scoreFunction" => "test.calculateEnergy"
215
+ ]
216
+ ]
217
+ ]);
218
+
219
+ // La fonction de score factice retourne une énergie *pire* que celle existante. // @phpstan-ignore-line
220
+ FunctionRegistry::register('test.calculateEnergy', function ($solution, $payload) {
221
+ return 1200.0;
222
+ });
223
+
224
+ $storeMock->method('get') // @phpstan-ignore-line
225
+ ->with("problem-state:{$problemId}")
226
+ ->willReturn(['bestEnergy' => 1000.0]);
227
+
228
+ // On s'attend à ce que `set` ne soit JAMAIS appelé car la solution n'est pas meilleure
229
+ $storeMock->expects($this->never())->method('set');
230
+
231
+ $manager = ProblemManager::getInstance($this->configPath, $storeMock);
232
+
233
+ $worseSolution = [
234
+ 'solution' => [3, 2, 1], // Le contenu de la solution n'a pas d'importance pour ce test
235
+ 'energy' => 1200.0 // C'est une moins bonne solution
236
+ ];
237
+ $manager->integrateSolution($problemId, $worseSolution);
238
+ }
239
+
240
+ public function testIntegrateParetoFront(): void
241
+ {
242
+ $problemId = 'pareto-challenge';
243
+
244
+ $storeMock = $this->createMock(IStore::class);
245
+
246
+ $this->createConfigFile([
247
+ [
248
+ "id" => "pareto-challenge",
249
+ "workUnit" => ["type" => "multi_objective_genetic_algorithm", "solverName" => "test.solver"]
250
+ ]
251
+ ]);
252
+ // Simule le comportement réel : le store contient déjà l'état initial.
253
+ $initialState = ['paretoFront' => [['solution' => 'A', 'objectives' => [10, 20]]]];
254
+ $storeMock->method('get')
255
+ ->with("problem-state:{$problemId}")
256
+ ->willReturn($initialState);
257
+
258
+ // A new solution that dominates the existing one.
259
+ $newFront = [['solution' => 'B', 'objectives' => [5, 15]]];
260
+
261
+ $storeMock->expects($this->once())
262
+ ->method('set')
263
+ ->with(
264
+ "problem-state:{$problemId}",
265
+ $this->callback(function ($state) {
266
+ // The new front should contain only the new, dominant solution 'B'.
267
+ return isset($state['paretoFront']) && count($state['paretoFront']) === 1 && $state['paretoFront'][0]['solution'] === 'B';
268
+ })
269
+ );
270
+
271
+ $manager = ProblemManager::getInstance($this->configPath, $storeMock);
272
+ $manager->integrateSolution($problemId, ['paretoFront' => $newFront]);
273
+ }
274
+
275
+ public function testLoadProblemsFromStore(): void
276
+ {
277
+ $store = new InMemoryStore();
278
+ $problemId = 'problem-1';
279
+
280
+ $this->createConfigFile([
281
+ [
282
+ "id" => "problem-1",
283
+ "workUnit" => ["type" => "simulated_annealing_iterations", "baseIterations" => 10000]
284
+ ]
285
+ ]);
286
+ $initialState = ['bestEnergy' => 5000.0, 'bestSolution' => [1, 2, 3]];
287
+ $store->set("problem-state:{$problemId}", $initialState);
288
+
289
+ $manager = ProblemManager::getInstance($this->configPath, $store);
290
+ $work = $manager->dispatchWork(0.1); // Dispatch 'problem-1'
291
+
292
+ $this->assertEquals($problemId, $work['problemId']);
293
+ $this->assertEquals($initialState['bestSolution'], $work['task']['initialSolution']);
294
+ }
295
+ }
@@ -0,0 +1,81 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use PHPUnit\Framework\TestCase;
8
+ use Anonympins\Fingerprint\RequestContext;
9
+ use Anonympins\Fingerprint\Utils\RequestUtils;
10
+
11
+ class RequestUtilsTest extends TestCase
12
+ {
13
+ private function createRequestContext(array $overrides = []): RequestContext
14
+ {
15
+ $defaults = [
16
+ 'clientIp' => '127.0.0.1',
17
+ 'path' => '/',
18
+ 'headers' => ['user-agent' => 'Test UA'],
19
+ 'query' => [],
20
+ 'body' => null,
21
+ 'cookies' => [],
22
+ 'httpVersion' => '1.1',
23
+ ];
24
+ $params = array_merge($defaults, $overrides);
25
+ return new RequestContext(
26
+ $params['clientIp'], $params['path'], $params['headers'],
27
+ $params['query'], $params['body'], $params['cookies'], $params['httpVersion']
28
+ );
29
+ }
30
+
31
+ public function testGetClickVarianceScoreReturnsZeroForNoHistory(): void
32
+ {
33
+ $context = $this->createRequestContext([
34
+ 'headers' => ['x-behavior-metrics' => json_encode([])]
35
+ ]);
36
+ $result = RequestUtils::getClickVarianceScore($context);
37
+ $this->assertEquals(0.0, $result['clickVarianceScore']);
38
+ }
39
+
40
+ public function testGetClickVarianceScoreReturnsZeroForInsufficientClicks(): void
41
+ {
42
+ $metrics = [
43
+ 'clicksHistory' => [
44
+ ['x' => 10, 'y' => 10, 'targetId' => 'hash1'],
45
+ ['x' => 11, 'y' => 11, 'targetId' => 'hash1'],
46
+ ['x' => 100, 'y' => 100, 'targetId' => 'hash2']
47
+ ]
48
+ ];
49
+ $context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
50
+ $result = RequestUtils::getClickVarianceScore($context);
51
+ $this->assertEquals(0.0, $result['clickVarianceScore']);
52
+ }
53
+
54
+ public function testGetClickVarianceScoreReturnsHighScoreForLowVariance(): void
55
+ {
56
+ $metrics = [
57
+ 'clicksHistory' => [
58
+ ['x' => 100, 'y' => 100, 'targetId' => 'hash1'],
59
+ ['x' => 100.1, 'y' => 100.2, 'targetId' => 'hash1'],
60
+ ['x' => 99.9, 'y' => 99.8, 'targetId' => 'hash1']
61
+ ]
62
+ ];
63
+ $context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
64
+ $result = RequestUtils::getClickVarianceScore($context);
65
+ $this->assertGreaterThan(90, $result['clickVarianceScore']);
66
+ }
67
+
68
+ public function testGetClickVarianceScoreReturnsLowScoreForHighVariance(): void
69
+ {
70
+ $metrics = [
71
+ 'clicksHistory' => [
72
+ ['x' => 105, 'y' => 110, 'targetId' => 'hash1'],
73
+ ['x' => 98, 'y' => 102, 'targetId' => 'hash1'],
74
+ ['x' => 112, 'y' => 95, 'targetId' => 'hash1']
75
+ ]
76
+ ];
77
+ $context = $this->createRequestContext(['headers' => ['x-behavior-metrics' => json_encode($metrics)]]);
78
+ $result = RequestUtils::getClickVarianceScore($context);
79
+ $this->assertEquals(0.0, $result['clickVarianceScore']);
80
+ }
81
+ }
@@ -0,0 +1,9 @@
1
+ [
2
+ {
3
+ "id": "problem-1",
4
+ "workUnit": {
5
+ "type": "simulated_annealing_iterations",
6
+ "baseIterations": 10000
7
+ }
8
+ }
9
+ ]
@@ -0,0 +1,145 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Utils;
6
+
7
+ class BigInt
8
+ {
9
+ private static bool $useGmp;
10
+ /** @var \GMP|string */
11
+ private $value;
12
+
13
+ /**
14
+ * @param \GMP|string|int $number Le nombre initial.
15
+ */
16
+ public function __construct($number)
17
+ {
18
+ if (!isset(self::$useGmp)) {
19
+ self::$useGmp = extension_loaded('gmp');
20
+ }
21
+
22
+ if (self::$useGmp) {
23
+ if ($number instanceof \GMP) {
24
+ $this->value = $number;
25
+ } else {
26
+ $this->value = gmp_init($number);
27
+ }
28
+ } else {
29
+ if ($number instanceof BigInt) {
30
+ $this->value = $number->value;
31
+ } else {
32
+ $this->value = (string)$number;
33
+ }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Crée une instance à partir d'une chaîne hexadécimale.
39
+ */
40
+ public static function fromHex(string $hex): BigInt
41
+ {
42
+ if (self::$useGmp) {
43
+ return new self(gmp_init($hex, 16));
44
+ } else {
45
+ $dec = '0';
46
+ $len = strlen($hex);
47
+ for ($i = 0; $i < $len; $i++) {
48
+ $dec = bcadd(bcmul($dec, '16'), (string)hexdec($hex[$i]));
49
+ }
50
+ return new self($dec);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Compare cette instance avec une autre.
56
+ * @return int < 0 si this < other, 0 si this == other, > 0 si this > other.
57
+ */
58
+ public function compareTo(BigInt $other): int
59
+ {
60
+ if (self::$useGmp) {
61
+ return gmp_cmp($this->value, $other->value);
62
+ } else {
63
+ return bccomp($this->value, $other->value);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Effectue un décalage de bits vers la gauche (<<).
69
+ */
70
+ public function shiftLeft(int $bits): BigInt
71
+ {
72
+ if (self::$useGmp) {
73
+ return new self(gmp_mul($this->value, gmp_pow("2", $bits)));
74
+ } else {
75
+ return new self(bcmul($this->value, bcpow("2", (string)$bits)));
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Effectue un décalage de bits vers la droite (>>).
81
+ */
82
+ public function shiftRight(int $bits): BigInt
83
+ {
84
+ if (self::$useGmp) {
85
+ return new self(gmp_div_q($this->value, gmp_pow("2", $bits)));
86
+ } else {
87
+ return new self(bcdiv($this->value, bcpow("2", (string)$bits)));
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Retourne la représentation en chaîne de caractères.
93
+ */
94
+ public function __toString(): string
95
+ {
96
+ if (self::$useGmp) {
97
+ return gmp_strval($this->value);
98
+ } else {
99
+ return $this->value;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Retourne la représentation hexadécimale.
105
+ */
106
+ public function toHex(): string
107
+ {
108
+ if (self::$useGmp) {
109
+ return gmp_strval($this->value, 16);
110
+ } else {
111
+ $hex = '';
112
+ $dec = $this->value;
113
+ while (bccomp($dec, '0') > 0) {
114
+ $rem = bcmod($dec, '16');
115
+ $hex = dechex((int)$rem) . $hex;
116
+ $dec = bcdiv($dec, '16');
117
+ }
118
+ return $hex ?: '0';
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Crée une instance à partir d'une puissance de 2.
124
+ */
125
+ public static function pow(int $base, int $exp): BigInt
126
+ {
127
+ if (self::$useGgmp) {
128
+ return new self(gmp_pow((string)$base, $exp));
129
+ } else {
130
+ return new self(bcpow((string)$base, (string)$exp));
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Soustrait un autre BigInt.
136
+ */
137
+ public function sub(BigInt $other): BigInt
138
+ {
139
+ if (self::$useGmp) {
140
+ return new self(gmp_sub($this->value, $other->value));
141
+ } else {
142
+ return new self(bcsub($this->value, $other->value));
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,100 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Utils;
6
+
7
+ /**
8
+ * Implémentation PHP d'une liste de blocage IP/CIDR, similaire à Node.js `net.BlockList`.
9
+ */
10
+ class BlockList
11
+ {
12
+ /**
13
+ * @var array<string> Liste des IPs ou CIDR à bloquer.
14
+ */
15
+ private array $entries = [];
16
+
17
+ /**
18
+ * Ajoute une adresse IP ou une plage CIDR à la liste.
19
+ * @param string $entry Une adresse IP (ex: '192.168.1.1') ou une plage CIDR (ex: '192.168.1.0/24').
20
+ * @return void
21
+ */
22
+ public function add(string $entry): void
23
+ {
24
+ $this->entries[] = $entry;
25
+ }
26
+
27
+ /**
28
+ * Vérifie si une adresse IP est présente dans la liste de blocage.
29
+ * @param string $ip L'adresse IP à vérifier.
30
+ * @return bool True si l'IP est bloquée, false sinon.
31
+ */
32
+ public function check(string $ip): bool
33
+ {
34
+ foreach ($this->entries as $entry) {
35
+ if (str_contains($entry, '/')) {
36
+ // C'est une plage CIDR
37
+ if ($this->ipInCidr($ip, $entry)) {
38
+ return true;
39
+ }
40
+ } else {
41
+ // C'est une IP directe
42
+ if ($ip === $entry) {
43
+ return true;
44
+ }
45
+ }
46
+ }
47
+ return false;
48
+ }
49
+
50
+ /**
51
+ * Vérifie si une adresse IP se trouve dans une plage CIDR donnée.
52
+ * @param string $ip L'adresse IP à vérifier.
53
+ * @param string $cidr La plage CIDR (ex: '192.168.1.0/24').
54
+ * @return bool True si l'IP est dans la plage, false sinon.
55
+ */
56
+ private function ipInCidr(string $ip, string $cidr): bool
57
+ {
58
+ [$network, $mask] = explode('/', $cidr);
59
+ $mask = (int)$mask;
60
+
61
+ if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
62
+ // IPv6
63
+ $ipBinary = inet_pton($ip);
64
+ $networkBinary = inet_pton($network);
65
+
66
+ if ($ipBinary === false || $networkBinary === false) {
67
+ return false;
68
+ }
69
+
70
+ $ipHex = bin2hex($ipBinary);
71
+ $networkHex = bin2hex($networkBinary);
72
+
73
+ $numBytes = (int)ceil($mask / 8);
74
+ $compareLength = $numBytes * 2;
75
+
76
+ if (substr($ipHex, 0, $compareLength) !== substr($networkHex, 0, $compareLength)) {
77
+ return false;
78
+ }
79
+
80
+ if ($mask % 8 !== 0) {
81
+ $bitMask = (0xFF << (8 - ($mask % 8))) & 0xFF;
82
+ $ipByte = hexdec(substr($ipHex, $numBytes * 2 - 2, 2));
83
+ $networkByte = hexdec(substr($networkHex, $numBytes * 2 - 2, 2));
84
+ return ($ipByte & $bitMask) === ($networkByte & $bitMask);
85
+ }
86
+
87
+ return true;
88
+
89
+ } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
90
+ // IPv4
91
+ $ipLong = ip2long($ip);
92
+ $networkLong = ip2long($network);
93
+ $wildcard = pow(2, (32 - $mask)) - 1;
94
+ $netmask = ~$wildcard;
95
+ return (($ipLong & $netmask) === ($networkLong & $netmask));
96
+ }
97
+
98
+ return false;
99
+ }
100
+ }
@@ -0,0 +1,30 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Utils;
6
+
7
+ /**
8
+ * Un simple wrapper de logger pour passer les données à une fonction de rappel.
9
+ */
10
+ class Logger
11
+ {
12
+ /**
13
+ * @var callable
14
+ */
15
+ private $callback;
16
+
17
+ public function __construct(callable $callback)
18
+ {
19
+ $this->callback = $callback;
20
+ }
21
+
22
+ public function log(string $level, string $type, array $data): void
23
+ {
24
+ $logEntry = array_merge($data, [
25
+ 'type' => $type,
26
+ 'timestamp' => (int)floor(microtime(true) * 1000),
27
+ ]);
28
+ ($this->callback)($logEntry);
29
+ }
30
+ }