@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
|
@@ -31,6 +31,137 @@
|
|
|
31
31
|
private static ?array $yandexEntries = null;
|
|
32
32
|
private static ?array $bingbotEntries = null;
|
|
33
33
|
|
|
34
|
+
private static array $dnsCircuitBreaker = [
|
|
35
|
+
'state' => 'CLOSED',
|
|
36
|
+
'failureCount' => 0,
|
|
37
|
+
'lastStateChange' => 0,
|
|
38
|
+
'threshold' => 5,
|
|
39
|
+
'cooldown' => 30, // seconds
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
private static function recordDnsSuccess(): void
|
|
43
|
+
{
|
|
44
|
+
self::$dnsCircuitBreaker['failureCount'] = 0;
|
|
45
|
+
self::$dnsCircuitBreaker['state'] = 'CLOSED';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private static function recordDnsFailure(): void
|
|
49
|
+
{
|
|
50
|
+
self::$dnsCircuitBreaker['failureCount']++;
|
|
51
|
+
if (self::$dnsCircuitBreaker['failureCount'] >= self::$dnsCircuitBreaker['threshold']) {
|
|
52
|
+
self::$dnsCircuitBreaker['state'] = 'OPEN';
|
|
53
|
+
self::$dnsCircuitBreaker['lastStateChange'] = microtime(true);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private static function canAttemptDns(): bool
|
|
58
|
+
{
|
|
59
|
+
if (self::$dnsCircuitBreaker['state'] === 'CLOSED') {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (self::$dnsCircuitBreaker['state'] === 'OPEN') {
|
|
63
|
+
if (microtime(true) - self::$dnsCircuitBreaker['lastStateChange'] >= self::$dnsCircuitBreaker['cooldown']) {
|
|
64
|
+
self::$dnsCircuitBreaker['state'] = 'HALF-OPEN';
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return true; // HALF-OPEN
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private static function getReverseDnsName(string $ip): ?string
|
|
73
|
+
{
|
|
74
|
+
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
|
75
|
+
return implode('.', array_reverse(explode('.', $ip))) . '.in-addr.arpa';
|
|
76
|
+
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
77
|
+
$hex = bin2hex((string)inet_pton($ip));
|
|
78
|
+
return implode('.', array_reverse(str_split($hex))) . '.ip6.arpa';
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static function dnsResolveUdp(string $query, string $type, float $timeout = 0.5): ?array
|
|
84
|
+
{
|
|
85
|
+
$dnsServer = '8.8.8.8';
|
|
86
|
+
$port = 53;
|
|
87
|
+
$fp = @stream_socket_client("udp://$dnsServer:$port", $errno, $errstr, $timeout);
|
|
88
|
+
if (!$fp) return null;
|
|
89
|
+
stream_set_timeout($fp, 0, (int)($timeout * 1000000));
|
|
90
|
+
|
|
91
|
+
$id = rand(10000, 65000);
|
|
92
|
+
$header = pack('n6', $id, 0x0100, 1, 0, 0, 0);
|
|
93
|
+
|
|
94
|
+
$qType = ($type === 'PTR') ? 12 : (($type === 'AAAA') ? 28 : 1);
|
|
95
|
+
$parts = explode('.', $query);
|
|
96
|
+
$qName = '';
|
|
97
|
+
foreach ($parts as $part) {
|
|
98
|
+
$qName .= chr(strlen($part)) . $part;
|
|
99
|
+
}
|
|
100
|
+
$qName .= chr(0);
|
|
101
|
+
$question = $qName . pack('n2', $qType, 1);
|
|
102
|
+
|
|
103
|
+
if (!@fwrite($fp, $header . $question)) {
|
|
104
|
+
fclose($fp);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
$response = @fread($fp, 512);
|
|
109
|
+
fclose($fp);
|
|
110
|
+
if (!$response || strlen($response) < 12) return null;
|
|
111
|
+
|
|
112
|
+
$resHeader = unpack('n6', substr($response, 0, 12));
|
|
113
|
+
if ($resHeader[1] !== $id) return null;
|
|
114
|
+
|
|
115
|
+
$answersCount = $resHeader[4];
|
|
116
|
+
if ($answersCount <= 0) return [];
|
|
117
|
+
|
|
118
|
+
$offset = 12 + strlen($question);
|
|
119
|
+
$results = [];
|
|
120
|
+
for ($i = 0; $i < $answersCount; $i++) {
|
|
121
|
+
if ($offset + 12 > strlen($response)) break;
|
|
122
|
+
if ((ord($response[$offset]) & 0xc0) === 0xc0) {
|
|
123
|
+
$offset += 2;
|
|
124
|
+
} else {
|
|
125
|
+
while ($offset < strlen($response) && ord($response[$offset]) !== 0) {
|
|
126
|
+
$offset += 1 + ord($response[$offset]);
|
|
127
|
+
}
|
|
128
|
+
$offset++;
|
|
129
|
+
}
|
|
130
|
+
if ($offset + 10 > strlen($response)) break;
|
|
131
|
+
$typeAndClass = unpack('n2type_class/Nttl/nlen', substr($response, $offset, 10));
|
|
132
|
+
$rType = $typeAndClass['type_class1'];
|
|
133
|
+
$rLen = $typeAndClass['len'];
|
|
134
|
+
$offset += 10;
|
|
135
|
+
if ($offset + $rLen > strlen($response)) break;
|
|
136
|
+
$rData = substr($response, $offset, $rLen);
|
|
137
|
+
$offset += $rLen;
|
|
138
|
+
|
|
139
|
+
if ($rType === 12) {
|
|
140
|
+
$ptrName = '';
|
|
141
|
+
$p = 0;
|
|
142
|
+
while ($p < strlen($rData)) {
|
|
143
|
+
$l = ord($rData[$p]);
|
|
144
|
+
if ($l === 0) break;
|
|
145
|
+
if (($l & 0xc0) === 0xc0) {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
$ptrName .= substr($rData, $p + 1, $l) . '.';
|
|
149
|
+
$p += 1 + $l;
|
|
150
|
+
}
|
|
151
|
+
$results[] = rtrim($ptrName, '.');
|
|
152
|
+
} elseif ($rType === 1) {
|
|
153
|
+
if ($rLen === 4) {
|
|
154
|
+
$results[] = long2ip(unpack('N', $rData)[1]);
|
|
155
|
+
}
|
|
156
|
+
} elseif ($rType === 28) {
|
|
157
|
+
if ($rLen === 16) {
|
|
158
|
+
$results[] = inet_ntop($rData);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return $results;
|
|
163
|
+
}
|
|
164
|
+
|
|
34
165
|
public function __construct(array $securityConfig)
|
|
35
166
|
{
|
|
36
167
|
$this->isProduction = ($_ENV['APP_ENV'] ?? getenv('APP_ENV')) === 'production';
|
|
@@ -387,13 +518,19 @@
|
|
|
387
518
|
|
|
388
519
|
$deviceId = bin2hex(random_bytes(16)); // UUID-like
|
|
389
520
|
|
|
521
|
+
$isHttps = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
|
|
522
|
+
(isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) ||
|
|
523
|
+
($context->getHeader('x-forwarded-proto') === 'https');
|
|
524
|
+
$secureOption = $isHttps || $this->isProduction;
|
|
525
|
+
|
|
390
526
|
// Préparer le cookie à envoyer
|
|
391
527
|
$newCookie = [
|
|
392
528
|
'name' => 'device_id',
|
|
393
529
|
'value' => $deviceId,
|
|
394
530
|
'options' => [
|
|
395
531
|
'httponly' => true,
|
|
396
|
-
'secure' => $
|
|
532
|
+
'secure' => $secureOption,
|
|
533
|
+
'partitioned' => $secureOption,
|
|
397
534
|
'samesite' => 'Strict',
|
|
398
535
|
'path' => '/',
|
|
399
536
|
]
|
|
@@ -852,7 +989,7 @@
|
|
|
852
989
|
]);
|
|
853
990
|
if (json_last_error() === JSON_ERROR_NONE) {
|
|
854
991
|
// @phpstan-ignore-next-line - L'instance est gérée par le singleton
|
|
855
|
-
|
|
992
|
+
$defaultPath = dirname(__DIR__, 2) . '/config/problems.config.json';
|
|
856
993
|
$configPath = $this->securityConfig['usefulWorkConfigPath'] ?? (file_exists($defaultPath) ? $defaultPath : null);
|
|
857
994
|
$problemManager = \Anonympins\Fingerprint\ProblemManager::getInstance($configPath, $store);
|
|
858
995
|
// FIX: La solution est directement le $workResult, pas une sous-propriété.
|
|
@@ -871,15 +1008,31 @@
|
|
|
871
1008
|
if ($isValid) {
|
|
872
1009
|
$store->delete("secret:{$powNonce}");
|
|
873
1010
|
$ticketTtl = $this->securityConfig['ticketMaxAge'] ?? 3600000;
|
|
874
|
-
|
|
1011
|
+
MetricsManager::incrementCounter('challenges_solved_total');
|
|
875
1012
|
$this->log('Challenge solution valid - issuing ticket', ['ticketMaxAge' => $ticketTtl]);
|
|
876
|
-
|
|
1013
|
+
|
|
1014
|
+
$isHttps = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
|
|
1015
|
+
(isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) ||
|
|
1016
|
+
($context->getHeader('x-forwarded-proto') === 'https');
|
|
1017
|
+
$secureOption = $isHttps || $this->isProduction;
|
|
1018
|
+
|
|
877
1019
|
return [
|
|
878
1020
|
'action' => 'redirect',
|
|
879
1021
|
'path' => RequestUtils::cleanUrlFromPowParams($challengeContext['originalPath'] ?? '/', $context->query),
|
|
880
1022
|
'score' => 0.0,
|
|
881
1023
|
'vector' => ['challenge_solved' => 100],
|
|
882
|
-
'cookie' => [
|
|
1024
|
+
'cookie' => [
|
|
1025
|
+
'name' => 'pow_clearance',
|
|
1026
|
+
'value' => $ticket,
|
|
1027
|
+
'options' => [
|
|
1028
|
+
'httponly' => true,
|
|
1029
|
+
'secure' => $secureOption,
|
|
1030
|
+
'samesite' => 'Strict',
|
|
1031
|
+
'expires' => time() + ($ticketTtl / 1000),
|
|
1032
|
+
'path' => '/',
|
|
1033
|
+
'partitioned' => $secureOption,
|
|
1034
|
+
]
|
|
1035
|
+
]
|
|
883
1036
|
];
|
|
884
1037
|
}
|
|
885
1038
|
}
|
|
@@ -888,7 +1041,7 @@
|
|
|
888
1041
|
$this->log('Challenge solution invalid or context expired', ['nonce' => $powNonce], 'warn');
|
|
889
1042
|
$suspicionVector['honeypotScore'] = 100.0;
|
|
890
1043
|
}
|
|
891
|
-
|
|
1044
|
+
|
|
892
1045
|
// 3. Vérifier un ticket existant
|
|
893
1046
|
$hasValidTicket = false;
|
|
894
1047
|
$powCookie = $context->cookies['pow_clearance'] ?? null;
|
|
@@ -903,7 +1056,7 @@
|
|
|
903
1056
|
// $this->log('Valid clearance ticket found');
|
|
904
1057
|
// return ['action' => 'next', 'score' => 0.0, 'vector' => ['ticket_valid' => 100]];
|
|
905
1058
|
}
|
|
906
|
-
|
|
1059
|
+
|
|
907
1060
|
// 4. Calculer le vecteur et le score de suspicion
|
|
908
1061
|
// Résoudre l'identité et vérifier le statut "condamné"
|
|
909
1062
|
$store = StoreManager::getStore();
|
|
@@ -934,7 +1087,7 @@
|
|
|
934
1087
|
RequestUtils::updateSubnetMetrics($context, $deviceId, $finalScore);
|
|
935
1088
|
MetricsManager::observeValue('suspicion_score', $finalScore, ['action' => 'high_score_subnet_update']);
|
|
936
1089
|
}
|
|
937
|
-
|
|
1090
|
+
|
|
938
1091
|
// Logique pour challenger les nouveaux appareils (déplacée ici pour avoir le score final)
|
|
939
1092
|
$isNewDevice = $identity['newCookie'] !== null;
|
|
940
1093
|
if ($isNewDevice && ($this->securityConfig['challengeNewDevices'] ?? false) && $finalScore < $thresholds['low']) {
|
|
@@ -944,7 +1097,7 @@
|
|
|
944
1097
|
]);
|
|
945
1098
|
$finalScore = (float)$thresholds['low'];
|
|
946
1099
|
}
|
|
947
|
-
|
|
1100
|
+
|
|
948
1101
|
// Vérifier les URL pièges (après calcul du score)
|
|
949
1102
|
$lastNonce = $deviceData['lastChallengeNonce'] ?? null;
|
|
950
1103
|
if ($lastNonce && ChallengeUtils::verifyTrapUrl($context->path, $context->query['sig'] ?? '', $lastNonce)) {
|
|
@@ -963,7 +1116,7 @@
|
|
|
963
1116
|
}
|
|
964
1117
|
return $decision;
|
|
965
1118
|
}
|
|
966
|
-
|
|
1119
|
+
|
|
967
1120
|
// 5. Prendre une décision basée sur le score - Vérifier le blocage d'abord.
|
|
968
1121
|
$blockThreshold = $thresholds['block'] ?? 95;
|
|
969
1122
|
if ($finalScore >= $blockThreshold) {
|
|
@@ -1001,13 +1154,13 @@
|
|
|
1001
1154
|
|
|
1002
1155
|
$highThreshold = $thresholds['high'] ?? 75;
|
|
1003
1156
|
$mustReChallenge = $finalScore >= $highThreshold && $hasValidTicket;
|
|
1004
|
-
|
|
1157
|
+
|
|
1005
1158
|
$lowThreshold = $thresholds['low'] ?? 20;
|
|
1006
1159
|
if (($finalScore >= $lowThreshold && !$hasValidTicket) || $mustReChallenge) {
|
|
1007
1160
|
if ($mustReChallenge) {
|
|
1008
1161
|
$this->log('High suspicion score detected - overriding valid ticket to re-issue challenge', ['finalScore' => $finalScore, 'deviceId' => $deviceId]);
|
|
1009
1162
|
}
|
|
1010
|
-
|
|
1163
|
+
|
|
1011
1164
|
// --- AJOUT: Limiteur de débit (Token Bucket) ---
|
|
1012
1165
|
$rateLimitPassed = ChallengeUtils::checkChallengeRateLimit($context->clientIp);
|
|
1013
1166
|
if (!$rateLimitPassed) {
|
|
@@ -1027,9 +1180,9 @@
|
|
|
1027
1180
|
}
|
|
1028
1181
|
return $decision;
|
|
1029
1182
|
}
|
|
1030
|
-
|
|
1183
|
+
|
|
1031
1184
|
$decision = ['action' => 'challenge', 'score' => $finalScore, 'vector' => $suspicionVector, 'status' => 403];
|
|
1032
|
-
|
|
1185
|
+
|
|
1033
1186
|
if ($this->dryRun) {
|
|
1034
1187
|
MetricsManager::incrementCounter('requests_total', ['status' => 'dry_run_challenge']);
|
|
1035
1188
|
$this->log("[Dry Run] Intended action: {$decision['action']}", ['score' => $decision['score']]);
|
|
@@ -1038,10 +1191,10 @@
|
|
|
1038
1191
|
unset($decision['status']);
|
|
1039
1192
|
return $decision;
|
|
1040
1193
|
}
|
|
1041
|
-
|
|
1194
|
+
|
|
1042
1195
|
MetricsManager::incrementCounter('requests_total', ['status' => 'challenged']);
|
|
1043
1196
|
$this->log('Suspicious request - selecting challenge type', ['finalScore' => $finalScore]);
|
|
1044
|
-
|
|
1197
|
+
|
|
1045
1198
|
$nonce = bin2hex(random_bytes(16));
|
|
1046
1199
|
$clientSecret = bin2hex(random_bytes(16));
|
|
1047
1200
|
$highThreshold = $thresholds['high'] ?? 75;
|
|
@@ -1062,7 +1215,7 @@
|
|
|
1062
1215
|
|
|
1063
1216
|
if ($shouldUseUsefulWork) {
|
|
1064
1217
|
$this->log('Issuing a useful work challenge', ['finalScore' => $finalScore]);
|
|
1065
|
-
$defaultPath = dirname(__DIR__, 2) . '/problems.config.json';
|
|
1218
|
+
$defaultPath = dirname(__DIR__, 2) . '/config/problems.config.json';
|
|
1066
1219
|
$configPath = $this->securityConfig['usefulWorkConfigPath'] ?? (file_exists($defaultPath) ? $defaultPath : null);
|
|
1067
1220
|
$problemManager = ProblemManager::getInstance($configPath, $store);
|
|
1068
1221
|
$work = $problemManager->dispatchWork($finalScore);
|
|
@@ -1143,10 +1296,10 @@
|
|
|
1143
1296
|
// Alignement linéaire parfait du ratio d'effort CPU/Mémoire
|
|
1144
1297
|
$memActivationFactor = $suspicionFactor;
|
|
1145
1298
|
$memDifficulty = (int)round($memActivationFactor * 48); // 0 à 48MB
|
|
1146
|
-
|
|
1299
|
+
|
|
1147
1300
|
$originalFingerprint = RequestUtils::getCompositeDeviceHash($context);
|
|
1148
1301
|
$baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint);
|
|
1149
|
-
|
|
1302
|
+
|
|
1150
1303
|
$challengeContext = [
|
|
1151
1304
|
'clientSecret' => $clientSecret,
|
|
1152
1305
|
'cpuTarget' => $cpuChallengeDetails['target'],
|
|
@@ -1156,22 +1309,22 @@
|
|
|
1156
1309
|
'baseBlock' => $baseBlock,
|
|
1157
1310
|
'originalPath' => $context->path,
|
|
1158
1311
|
];
|
|
1159
|
-
|
|
1312
|
+
|
|
1160
1313
|
$store->set("secret:{$nonce}", $challengeContext, $this->securityConfig['challengeTtl'] ?? 300);
|
|
1161
|
-
|
|
1314
|
+
|
|
1162
1315
|
// Associer le nonce au device pour la vérification des URL pièges
|
|
1163
1316
|
if ($deviceData) {
|
|
1164
1317
|
$deviceData['lastChallengeNonce'] = $nonce;
|
|
1165
1318
|
$store->set("device:{$deviceId}", $deviceData); // @phpstan-ignore-line
|
|
1166
1319
|
}
|
|
1167
|
-
|
|
1320
|
+
|
|
1168
1321
|
$trapUrls = [ChallengeUtils::generateTrapUrl($nonce), ChallengeUtils::generateTrapUrl($nonce)];
|
|
1169
1322
|
$this->log('Challenge issued', ['nonce' => $nonce, 'ttl' => $this->securityConfig['challengeTtl'] ?? 300]);
|
|
1170
|
-
|
|
1323
|
+
|
|
1171
1324
|
if ($this->logger) {
|
|
1172
1325
|
$this->logger->log('info', 'challenge_issued', ['deviceId' => $deviceId, 'score' => $finalScore, 'vector' => $suspicionVector]);
|
|
1173
1326
|
}
|
|
1174
|
-
|
|
1327
|
+
|
|
1175
1328
|
// Pour les API, retourner un challenge JSON
|
|
1176
1329
|
if ($isApiRequest) {
|
|
1177
1330
|
$challengePayload = [
|
|
@@ -1188,7 +1341,7 @@
|
|
|
1188
1341
|
} else {
|
|
1189
1342
|
// Pour les navigateurs, retourner une page HTML
|
|
1190
1343
|
$pageBody = ChallengeUtils::generateCombinedPoWChallengePage(
|
|
1191
|
-
$cpuChallengeDetails, $memDifficulty, $clientSecret,
|
|
1344
|
+
$cpuChallengeDetails, $memDifficulty, $clientSecret,
|
|
1192
1345
|
$this->securityConfig, $trapUrls, $originalFingerprint
|
|
1193
1346
|
);
|
|
1194
1347
|
$decision['body'] = $pageBody;
|
|
@@ -1210,12 +1363,12 @@
|
|
|
1210
1363
|
$response = ['action' => 'next', 'score' => $finalScore, 'vector' => $suspicionVector, 'intendedAction' => 'next'];
|
|
1211
1364
|
}
|
|
1212
1365
|
}
|
|
1213
|
-
|
|
1366
|
+
|
|
1214
1367
|
// Si un nouveau cookie d'identification a été généré, on l'ajoute à la réponse.
|
|
1215
1368
|
if (isset($context->newCookieForResponse)) {
|
|
1216
1369
|
$response['newCookieForResponse'] = $context->newCookieForResponse;
|
|
1217
1370
|
}
|
|
1218
|
-
|
|
1371
|
+
|
|
1219
1372
|
return $response;
|
|
1220
1373
|
}
|
|
1221
1374
|
|
|
@@ -1227,7 +1380,7 @@
|
|
|
1227
1380
|
if (empty($operationType) || empty($operationName)) {
|
|
1228
1381
|
return false;
|
|
1229
1382
|
}
|
|
1230
|
-
|
|
1383
|
+
|
|
1231
1384
|
$whitelistRules = $this->securityConfig['whitelist'] ?? [];
|
|
1232
1385
|
$graphqlRule = null;
|
|
1233
1386
|
foreach ($whitelistRules as $rule) {
|
|
@@ -1236,23 +1389,23 @@
|
|
|
1236
1389
|
break;
|
|
1237
1390
|
}
|
|
1238
1391
|
}
|
|
1239
|
-
|
|
1392
|
+
|
|
1240
1393
|
if (empty($graphqlRule['entries'])) {
|
|
1241
1394
|
return false;
|
|
1242
1395
|
}
|
|
1243
|
-
|
|
1396
|
+
|
|
1244
1397
|
foreach ($graphqlRule['entries'] as $entry) {
|
|
1245
1398
|
[$entryType, $entryName] = explode(':', $entry, 2);
|
|
1246
1399
|
if ($entryType !== $operationType) continue;
|
|
1247
|
-
|
|
1400
|
+
|
|
1248
1401
|
if ($entryName === $operationName || $entryName === '*') return true;
|
|
1249
|
-
|
|
1402
|
+
|
|
1250
1403
|
if (str_ends_with($entryName, '*') && str_starts_with($operationName, substr($entryName, 0, -1))) return true;
|
|
1251
1404
|
}
|
|
1252
|
-
|
|
1405
|
+
|
|
1253
1406
|
return false;
|
|
1254
1407
|
}
|
|
1255
|
-
|
|
1408
|
+
|
|
1256
1409
|
/**
|
|
1257
1410
|
* Vérifie si une requête provient d'un bot légitime et whitelisté (ex: Googlebot)
|
|
1258
1411
|
* en utilisant des recherches DNS inversées et directes. Le résultat est mis en cache.
|
|
@@ -1284,44 +1437,67 @@
|
|
|
1284
1437
|
if ($cachedStatus === 'verified') return true;
|
|
1285
1438
|
if ($cachedStatus === 'failed') return false;
|
|
1286
1439
|
|
|
1440
|
+
if (!self::canAttemptDns()) {
|
|
1441
|
+
return false;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1287
1444
|
try {
|
|
1288
|
-
// 1. Reverse DNS lookup
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1445
|
+
// 1. Reverse DNS lookup with strict 500ms timeout using our custom UDP DNS Client
|
|
1446
|
+
$revName = self::getReverseDnsName($context->client_ip);
|
|
1447
|
+
if (!$revName) {
|
|
1448
|
+
$store->set($cacheKey, 'failed', 300); // 5 min negative caching
|
|
1449
|
+
return false;
|
|
1450
|
+
}
|
|
1451
|
+
$hostnames = self::dnsResolveUdp($revName, 'PTR', 0.5);
|
|
1452
|
+
if ($hostnames === null || empty($hostnames)) {
|
|
1453
|
+
self::recordDnsFailure();
|
|
1454
|
+
$store->set($cacheKey, 'failed', 300); // 5 min negative caching
|
|
1293
1455
|
return false;
|
|
1294
1456
|
}
|
|
1295
1457
|
|
|
1296
1458
|
$validHostname = null;
|
|
1297
|
-
|
|
1298
|
-
$
|
|
1459
|
+
foreach ($hostnames as $hostname) {
|
|
1460
|
+
if (str_ends_with($hostname, $matchedRule['hostnameSuffix'])) {
|
|
1461
|
+
$validHostname = $hostname;
|
|
1462
|
+
break;
|
|
1463
|
+
}
|
|
1299
1464
|
}
|
|
1300
1465
|
|
|
1301
1466
|
if ($validHostname === null) {
|
|
1302
|
-
$store->set($cacheKey, 'failed',
|
|
1467
|
+
$store->set($cacheKey, 'failed', 300); // 5 min negative caching
|
|
1303
1468
|
return false;
|
|
1304
1469
|
}
|
|
1305
|
-
|
|
1306
|
-
// 2. Forward DNS lookup
|
|
1307
|
-
$addresses = array_merge(dns_get_record($validHostname, DNS_A) ?: [], dns_get_record($validHostname, DNS_AAAA) ?: []);
|
|
1470
|
+
|
|
1471
|
+
// 2. Forward DNS lookup with strict 500ms timeout
|
|
1308
1472
|
$ips = [];
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
$ips[] = $address['ipv6'];
|
|
1315
|
-
}
|
|
1473
|
+
$resolvedA = self::dnsResolveUdp($validHostname, 'A', 0.5);
|
|
1474
|
+
if ($resolvedA === null) {
|
|
1475
|
+
self::recordDnsFailure();
|
|
1476
|
+
$store->set($cacheKey, 'failed', 300); // 5 min negative caching
|
|
1477
|
+
return false;
|
|
1316
1478
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1479
|
+
$ips = array_merge($ips, $resolvedA);
|
|
1480
|
+
|
|
1481
|
+
$resolvedAaaa = self::dnsResolveUdp($validHostname, 'AAAA', 0.5);
|
|
1482
|
+
if ($resolvedAaaa === null) {
|
|
1483
|
+
self::recordDnsFailure();
|
|
1484
|
+
$store->set($cacheKey, 'failed', 300); // 5 min negative caching
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
$ips = array_merge($ips, $resolvedAaaa);
|
|
1488
|
+
|
|
1489
|
+
if (in_array($context->client_ip, $ips, true)) {
|
|
1490
|
+
self::recordDnsSuccess();
|
|
1319
1491
|
$store->set($cacheKey, 'verified', 86400);
|
|
1320
1492
|
return true;
|
|
1321
1493
|
}
|
|
1322
|
-
} catch (\Exception $e) {
|
|
1494
|
+
} catch (\Exception $e) {
|
|
1495
|
+
self::recordDnsFailure();
|
|
1496
|
+
$store->set($cacheKey, 'failed', 300); // 5 min negative caching
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1323
1499
|
|
|
1324
|
-
$store->set($cacheKey, 'failed',
|
|
1500
|
+
$store->set($cacheKey, 'failed', 300); // 5 min negative caching
|
|
1325
1501
|
return false;
|
|
1326
1502
|
}
|
|
1327
1503
|
|
|
@@ -224,31 +224,38 @@ class Optimization
|
|
|
224
224
|
*/
|
|
225
225
|
public static function benfordTest(array $numbers): float // Rendre la méthode publique et statique
|
|
226
226
|
{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
$leadingDigits = array_map(function ($n) {
|
|
232
|
-
$str = ltrim((string)$n, '0.');
|
|
233
|
-
return $str[0] ?? '';
|
|
234
|
-
}, $numbers);
|
|
235
|
-
|
|
236
|
-
$leadingDigits = array_filter($leadingDigits, fn ($d) => $d >= '1' && $d <= '9');
|
|
227
|
+
$counts = array_fill(1, 9, 0);
|
|
228
|
+
$validCount = 0;
|
|
237
229
|
|
|
238
|
-
|
|
239
|
-
|
|
230
|
+
foreach ($numbers as $n) {
|
|
231
|
+
if (is_string($n)) {
|
|
232
|
+
$n = (float)$n;
|
|
233
|
+
}
|
|
234
|
+
if (!is_numeric($n)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
$val = abs((float)$n);
|
|
238
|
+
if ($val == 0.0) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
$log = log10($val);
|
|
242
|
+
$factor = pow(10, (int)floor($log));
|
|
243
|
+
$digit = (int)floor($val / $factor);
|
|
244
|
+
if ($digit >= 1 && $digit <= 9) {
|
|
245
|
+
$counts[$digit]++;
|
|
246
|
+
$validCount++;
|
|
247
|
+
}
|
|
240
248
|
}
|
|
241
249
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
$counts[(int)$digit]++;
|
|
250
|
+
if ($validCount < 10) {
|
|
251
|
+
return 0.0;
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
$benfordDistribution = [1 => 30.1, 2 => 17.6, 3 => 12.5, 4 => 9.7, 5 => 7.9, 6 => 6.7, 7 => 5.8, 8 => 5.1, 9 => 4.6];
|
|
248
255
|
|
|
249
256
|
$totalDeviation = 0.0;
|
|
250
257
|
for ($i = 1; $i <= 9; $i++) {
|
|
251
|
-
$observedFrequency = ($counts[$i] /
|
|
258
|
+
$observedFrequency = ($counts[$i] / $validCount) * 100.0;
|
|
252
259
|
$expectedFrequency = $benfordDistribution[$i];
|
|
253
260
|
$totalDeviation += pow($observedFrequency - $expectedFrequency, 2);
|
|
254
261
|
}
|
|
@@ -9,6 +9,33 @@ namespace Anonympins\Fingerprint\Optimization;
|
|
|
9
9
|
*/
|
|
10
10
|
class OptimizationOperators
|
|
11
11
|
{
|
|
12
|
+
/**
|
|
13
|
+
* Crée une fonction de sélection par tournoi pour un algorithme génétique.
|
|
14
|
+
* @param array $options Options pour le tournoi.
|
|
15
|
+
* @return callable Une fonction de sélection.
|
|
16
|
+
*/
|
|
17
|
+
public static function createTournamentSelection(array $options = []): callable
|
|
18
|
+
{
|
|
19
|
+
$tournamentSize = $options['size'] ?? 5;
|
|
20
|
+
|
|
21
|
+
return function (array $population) use ($tournamentSize) {
|
|
22
|
+
$best = null;
|
|
23
|
+
$count = count($population);
|
|
24
|
+
|
|
25
|
+
for ($i = 0; $i < $tournamentSize; $i++) {
|
|
26
|
+
$individual = $population[random_int(0, $count - 1)];
|
|
27
|
+
$individualFitness = $individual['fitness'] ?? (isset($individual['objectives']) ? array_sum($individual['objectives']) : INF);
|
|
28
|
+
$bestFitness = $best !== null ? ($best['fitness'] ?? (isset($best['objectives']) ? array_sum($best['objectives']) : INF)) : INF;
|
|
29
|
+
|
|
30
|
+
if ($best === null || $individualFitness < $bestFitness) {
|
|
31
|
+
$best = $individual;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return $best ?? $population[random_int(0, $count - 1)];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
12
39
|
/**
|
|
13
40
|
* Génère un nombre flottant aléatoire cryptographiquement sûr entre 0 (inclus) et 1 (exclus).
|
|
14
41
|
*/
|
|
@@ -36,7 +36,7 @@ class ProblemManager
|
|
|
36
36
|
{
|
|
37
37
|
if (self::$instance === null) {
|
|
38
38
|
if ($configPath === null) {
|
|
39
|
-
$defaultPath = dirname(__DIR__, 2) . '/problems.config.json';
|
|
39
|
+
$defaultPath = dirname(__DIR__, 2) . '/config/problems.config.json';
|
|
40
40
|
$configPath = file_exists($defaultPath) ? $defaultPath : null;
|
|
41
41
|
}
|
|
42
42
|
// Si on essaie d'obtenir l'instance sans l'initialiser d'abord, c'est une erreur.
|