@anonympins/fingerprint 0.7.4 → 0.8.0

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 (64) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/public/anonympins-bot-mitigation-pow.zip +0 -0
  5. package/src/js/dynamic-wasm.js +218 -218
  6. package/src/js/fingerprint.client.js +1784 -1738
  7. package/src/js/fingerprint.client.obfuscated.js +1 -1
  8. package/src/js/fingerprint.js +489 -99
  9. package/src/js/fingerprint.utils.js +252 -2
  10. package/src/js/pow.worker.js +2 -2
  11. package/src/js/tests/cross_parity.test.js +3 -3
  12. package/src/js/tests/dns-circuit-breaker.test.js +6 -6
  13. package/src/js/tests/dynamic-wasm.test.js +77 -77
  14. package/src/js/tests/fingerprint.builder.test.js +76 -78
  15. package/src/js/tests/fingerprint.client.init.test.js +78 -0
  16. package/src/js/tests/fingerprint.client.test.js +16 -17
  17. package/src/js/tests/fingerprint.test.js +37 -12
  18. package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
  19. package/src/js/tests/library.test.js +94 -95
  20. package/src/js/tests/obfuscation.test.js +8 -8
  21. package/src/js/tests/quicFingerprint.test.js +47 -0
  22. package/src/js/tests/tcpFingerprint.test.js +4 -4
  23. package/src/js/tests/threatIntel.test.js +40 -1
  24. package/src/js/upow-model-task.js +155 -112
  25. package/src/php/Challenge/ChallengeUtils.php +392 -86
  26. package/src/php/Config/SecurityProfiles.php +79 -61
  27. package/src/php/DirectFingerprint.php +55 -8
  28. package/src/php/FingerprintBuilder.php +13 -13
  29. package/src/php/FingerprintClient.php +6 -6
  30. package/src/php/FingerprintEngine.php +2122 -1690
  31. package/src/php/Ja3AnomalyDetector.php +22 -22
  32. package/src/php/Optimization/FunctionRegistry.php +4 -4
  33. package/src/php/Optimization/Optimization.php +6 -6
  34. package/src/php/RequestContext.php +11 -11
  35. package/src/php/Store/InMemoryStore.php +96 -66
  36. package/src/php/Store/MongoDbStore.php +2 -2
  37. package/src/php/Store/RedisStore.php +1 -1
  38. package/src/php/Store/StoreManager.php +48 -35
  39. package/src/php/Tests/ChallengeUtilsTest.php +31 -0
  40. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  41. package/src/php/Tests/FingerprintClientTest.php +70 -70
  42. package/src/php/Tests/PowTest.php +37 -39
  43. package/src/php/Tests/QuicFingerprintTest.php +115 -53
  44. package/src/php/Tests/ThreatIntelTest.php +34 -0
  45. package/src/php/Utils/BigInt.php +202 -202
  46. package/src/php/Utils/BlockList.php +13 -13
  47. package/src/php/Utils/Logger.php +19 -1
  48. package/src/php/Utils/MaliciousPatterns.php +6 -6
  49. package/src/php/Utils/MetricsManager.php +209 -209
  50. package/src/php/Utils/RequestUtils.php +362 -1
  51. package/src/php/Utils/TLSClientHelloParser.php +385 -369
  52. package/src/php/WordPress/WpDbStore.php +7 -7
  53. package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
  54. package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
  55. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
  56. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
  57. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  58. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
  59. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  60. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
  61. package/src/php/WordPress/package.php +80 -101
  62. package/src/php/WordPress/readme.txt +21 -5
  63. package/src/php/bin/auto-tune.php +1 -1
  64. package/public/fingerprint-anti-bot.zip +0 -0
@@ -1,370 +1,386 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Utils;
6
-
7
- /**
8
- * Décode nativement un paquet TLS Client Hello binaire pour calculer l'empreinte JA3/JA4.
9
- */
10
- class TLSClientHelloParser
11
- {
12
- private const GREASE_VALUES = [
13
- 2570, 6682, 10794, 14906, 19018, 23130, 27242, 31354,
14
- 35466, 39578, 43690, 47802, 51914, 55926, 60038, 64150
15
- ];
16
-
17
- /**
18
- * Lit un entier encodé en longueur variable QUIC (RFC 9000 section 16).
19
- */
20
- public static function readVarInt(string $data, int &$offset): ?int
21
- {
22
- $len = strlen($data);
23
- if ($offset >= $len) {
24
- return null;
25
- }
26
-
27
- $first = ord($data[$offset]);
28
- $prefix = $first >> 6;
29
- $firstVal = $first & 0x3f;
30
-
31
- if ($prefix === 0) {
32
- $offset += 1;
33
- return $firstVal;
34
- } elseif ($prefix === 1) {
35
- if ($offset + 2 > $len) return null;
36
- $val = ($firstVal << 8) | ord($data[$offset + 1]);
37
- $offset += 2;
38
- return $val;
39
- } elseif ($prefix === 2) {
40
- if ($offset + 4 > $len) return null;
41
- $val = ($firstVal << 24) | (ord($data[$offset + 1]) << 16) | (ord($data[$offset + 2]) << 8) | ord($data[$offset + 3]);
42
- $offset += 4;
43
- return $val;
44
- } else {
45
- if ($offset + 8 > $len) return null;
46
- $val = $firstVal;
47
- for ($i = 1; $i < 8; $i++) {
48
- $val = ($val << 8) | ord($data[$offset + $i]);
49
- }
50
- $offset += 8;
51
- return $val;
52
- }
53
- }
54
-
55
- /**
56
- * Parse les paramètres de transport QUIC (RFC 9000 section 18.2).
57
- */
58
- public static function parseQuicTransportParameters(string $data): array
59
- {
60
- $params = [];
61
- $offset = 0;
62
- $len = strlen($data);
63
-
64
- while ($offset < $len) {
65
- $paramId = self::readVarInt($data, $offset);
66
- if ($paramId === null) break;
67
- $paramLen = self::readVarInt($data, $offset);
68
- if ($paramLen === null || $offset + $paramLen > $len) break;
69
-
70
- $paramValBytes = substr($data, $offset, $paramLen);
71
- $offset += $paramLen;
72
-
73
- if ($paramLen > 0 && in_array($paramId, [1, 3, 4, 5, 6, 7, 8, 9, 11, 14], true)) {
74
- $valOffset = 0;
75
- $numVal = self::readVarInt($paramValBytes, $valOffset);
76
- $params[$paramId] = $numVal ?? $paramValBytes;
77
- } else {
78
- $params[$paramId] = bin2hex($paramValBytes);
79
- }
80
- }
81
-
82
- return $params;
83
- }
84
-
85
- /**
86
- * Analyse l'ordre des trames de contrôle QUIC / HTTP/3 (SETTINGS, MAX_STREAMS, PRIORITY).
87
- */
88
- public static function parseQuicControlFrames(string $streamData): array
89
- {
90
- $frames = [];
91
- $frameOrder = [];
92
- $settings = [];
93
- $offset = 0;
94
- $len = strlen($streamData);
95
-
96
- if ($len === 0) {
97
- return ['frames' => [], 'frame_order' => '', 'settings' => []];
98
- }
99
-
100
- if (ord($streamData[0]) === 0x00) {
101
- $offset = 1; // Stream type Control Stream
102
- }
103
-
104
- while ($offset < $len) {
105
- $frameType = self::readVarInt($streamData, $offset);
106
- if ($frameType === null) break;
107
- $frameLen = self::readVarInt($streamData, $offset);
108
- if ($frameLen === null || $offset + $frameLen > $len) break;
109
-
110
- $payload = substr($streamData, $offset, $frameLen);
111
- $offset += $frameLen;
112
-
113
- $abbr = match ($frameType) {
114
- 0x04 => 's', // SETTINGS
115
- 0x12, 0x02 => 'm', // MAX_STREAMS
116
- 0x0f, 0xaf, 0xf0700 => 'p', // PRIORITY_UPDATE
117
- 0x10, 0x0d => 'd', // MAX_DATA
118
- 0x07 => 'g', // GOAWAY
119
- default => 'u'
120
- };
121
-
122
- $frames[] = ['type' => $frameType, 'length' => $frameLen];
123
- $frameOrder[] = $abbr;
124
-
125
- if ($frameType === 0x04) {
126
- $sOffset = 0;
127
- $sLen = strlen($payload);
128
- while ($sOffset < $sLen) {
129
- $sId = self::readVarInt($payload, $sOffset);
130
- if ($sId === null) break;
131
- $sVal = self::readVarInt($payload, $sOffset);
132
- if ($sVal === null) break;
133
- $settings[$sId] = $sVal;
134
- }
135
- }
136
- }
137
-
138
- return [
139
- 'frames' => $frames,
140
- 'frame_order' => implode(',', $frameOrder),
141
- 'settings' => $settings
142
- ];
143
- }
144
-
145
- public static function formatQuicFingerprint(array $params, string $priority = '', string $frameOrder = ''): string
146
- {
147
- $paramParts = [];
148
- foreach ($params as $k => $v) {
149
- $paramParts[] = "{$k}={$v}";
150
- }
151
- $fp = "1;" . implode(',', $paramParts);
152
- if ($priority !== '' || $frameOrder !== '') {
153
- $fp .= ";{$priority}";
154
- }
155
- if ($frameOrder !== '') {
156
- $fp .= ";{$frameOrder}";
157
- }
158
- return $fp;
159
- }
160
-
161
- /**
162
- * Parse le Client Hello brut et retourne l'empreinte JA3 et une approximation JA4.
163
- *
164
- * @param string $binary Le premier paquet TCP reçu sur la socket.
165
- * @return array{ja3_string: string, ja3_hash: string, ja4_raw: string}|null
166
- */
167
- public static function parse(string $binary): ?array
168
- {
169
- $len = strlen($binary);
170
- if ($len < 43) {
171
- return null; // Paquet trop court
172
- }
173
-
174
- // 1. Vérification du Record Layer Type (0x16 = Handshake)
175
- if (ord($binary[0]) !== 0x16) {
176
- return null;
177
- }
178
-
179
- // 2. Vérification du Handshake Type (0x01 = Client Hello)
180
- if (ord($binary[5]) !== 0x01) {
181
- return null;
182
- }
183
-
184
- $offset = 43; // Sauter l'en-tête, la version et le Random Client (32 octets)
185
- if ($len < $offset + 1) return null;
186
-
187
- // 3. Lecture du Session ID
188
- $sessionLen = ord($binary[$offset]);
189
- $offset += 1 + $sessionLen;
190
- if ($len < $offset + 2) return null;
191
-
192
- // 4. Lecture des Cipher Suites
193
- $ciphersLen = unpack('n', substr($binary, $offset, 2))[1];
194
- $offset += 2;
195
- if ($len < $offset + $ciphersLen + 1) return null;
196
-
197
- $ciphers = [];
198
- for ($i = 0; $i < $ciphersLen; $i += 2) {
199
- $ciphers[] = unpack('n', substr($binary, $offset + $i, 2))[1];
200
- }
201
- $offset += $ciphersLen;
202
-
203
- // 5. Lecture des Compression Methods
204
- $compressionLen = ord($binary[$offset]);
205
- $offset += 1 + $compressionLen;
206
- if ($len < $offset + 2) return null;
207
-
208
- // 6. Lecture des Extensions
209
- $extensionsLen = unpack('n', substr($binary, $offset, 2))[1];
210
- $offset += 2;
211
-
212
- $extensions = [];
213
- $curves = [];
214
- $points = [];
215
- $sigAlgs = [];
216
- $supportedVersions = [];
217
- $hasSni = false;
218
- $alpnProtocol = '';
219
- $quicParams = null;
220
-
221
- $extLimit = $offset + $extensionsLen;
222
- while ($offset < $extLimit && $offset + 4 <= $len) {
223
- $extType = unpack('n', substr($binary, $offset, 2))[1];
224
- $extLen = unpack('n', substr($binary, $offset + 2, 2))[1];
225
- $offset += 4;
226
-
227
- if ($offset + $extLen > $len) break;
228
-
229
- $extensions[] = $extType;
230
-
231
- if ($extType === 0) {
232
- $hasSni = true;
233
- } elseif ($extType === 10) { // Extension Supported Groups (Elliptic Curves)
234
- if ($extLen >= 2) {
235
- $curvesLen = unpack('n', substr($binary, $offset, 2))[1];
236
- for ($j = 2; $j < $curvesLen + 2; $j += 2) {
237
- $curves[] = unpack('n', substr($binary, $offset + $j, 2))[1];
238
- }
239
- }
240
- } elseif ($extType === 11) { // Extension EC Point Formats
241
- if ($extLen >= 1) {
242
- $pointsLen = ord($binary[$offset]);
243
- for ($j = 1; $j < $pointsLen + 1; $j++) {
244
- $points[] = ord($binary[$offset + $j]);
245
- }
246
- }
247
- } elseif ($extType === 13) { // Signature Algorithms
248
- if ($extLen >= 2 && $offset + 2 <= $len) {
249
- $sigAlgsLen = unpack('n', substr($binary, $offset, 2))[1];
250
- for ($j = 2; $j < $sigAlgsLen + 2; $j += 2) {
251
- if ($offset + $j + 2 <= $len && $j + 2 <= $extLen) {
252
- $sigAlgs[] = unpack('n', substr($binary, $offset + $j, 2))[1];
253
- }
254
- }
255
- }
256
- } elseif ($extType === 16) { // ALPN
257
- if ($extLen >= 3 && $offset + 2 <= $len) {
258
- $alpnListLen = unpack('n', substr($binary, $offset, 2))[1];
259
- if ($extLen >= 2 + $alpnListLen && $offset + 2 + $alpnListLen <= $len) {
260
- $alpnStrLen = ord($binary[$offset + 2]);
261
- if ($alpnListLen >= 1 + $alpnStrLen) {
262
- $alpnProtocol = substr($binary, $offset + 3, $alpnStrLen);
263
- }
264
- }
265
- }
266
- } elseif ($extType === 43) { // Supported Versions
267
- if ($extLen >= 1 && $offset + 1 <= $len) {
268
- $versionsLen = ord($binary[$offset]);
269
- for ($j = 1; $j < $versionsLen + 1; $j += 2) {
270
- if ($offset + $j + 2 <= $len && $j + 2 <= $extLen) {
271
- $supportedVersions[] = unpack('n', substr($binary, $offset + $j, 2))[1];
272
- }
273
- }
274
- }
275
- } elseif ($extType === 57 || $extType === 0xffa5) { // quic_transport_parameters (RFC 9001 / draft)
276
- if ($extLen > 0 && $offset + $extLen <= $len) {
277
- $quicData = substr($binary, $offset, $extLen);
278
- $quicParams = self::parseQuicTransportParameters($quicData);
279
- }
280
- }
281
- $offset += $extLen;
282
- }
283
-
284
- // Nettoyage des valeurs GREASE (RFC 8701) pour la conformité JA3
285
- $filterGrease = fn(array $arr) => array_values(array_filter($arr, fn($v) => !in_array($v, self::GREASE_VALUES, true)));
286
-
287
- $cleanCiphers = $filterGrease($ciphers);
288
- $cleanExtensions = $filterGrease($extensions);
289
- $cleanCurves = $filterGrease($curves);
290
- $cleanPoints = $filterGrease($points);
291
- $cleanSigAlgs = $filterGrease($sigAlgs);
292
- $cleanSupportedVersions = $filterGrease($supportedVersions);
293
-
294
- $sslVersion = unpack('n', substr($binary, 9, 2))[1];
295
- $ja3String = implode(',', [
296
- $sslVersion,
297
- implode('-', $cleanCiphers),
298
- implode('-', $cleanExtensions),
299
- implode('-', $cleanCurves),
300
- implode('-', $cleanPoints)
301
- ]);
302
-
303
- // Calcul natif de JA4
304
- $highestVersion = $sslVersion;
305
- if (!empty($cleanSupportedVersions)) {
306
- $highestVersion = max($cleanSupportedVersions);
307
- }
308
-
309
- $ja4Version = "12";
310
- if ($highestVersion === 0x0304) {
311
- $ja4Version = "13";
312
- } elseif ($highestVersion === 0x0303) {
313
- $ja4Version = "12";
314
- } elseif ($highestVersion === 0x0302) {
315
- $ja4Version = "11";
316
- } elseif ($highestVersion === 0x0301) {
317
- $ja4Version = "10";
318
- }
319
-
320
- $sniStatus = $hasSni ? "d" : "i";
321
- $numCiphers = min(99, count($cleanCiphers));
322
- $numExtensions = min(99, count($cleanExtensions));
323
-
324
- $ja4Alpn = "00";
325
- if ($alpnProtocol !== '') {
326
- $lenAlpn = strlen($alpnProtocol);
327
- if ($lenAlpn === 1) {
328
- $ja4Alpn = $alpnProtocol . $alpnProtocol;
329
- } else {
330
- $ja4Alpn = $alpnProtocol[0] . $alpnProtocol[$lenAlpn - 1];
331
- }
332
- }
333
-
334
- $ja4_a = "t" . $ja4Version . $sniStatus . sprintf("%02d", $numCiphers) . sprintf("%02d", $numExtensions) . $ja4Alpn;
335
-
336
- $sortedCiphers = $cleanCiphers;
337
- sort($sortedCiphers);
338
- $ciphersHex = array_map(fn($c) => sprintf('%04x', $c), $sortedCiphers);
339
- $ciphersString = implode(',', $ciphersHex);
340
- $ja4_b = substr(hash('sha256', $ciphersString), 0, 12);
341
-
342
- $sortedExtensions = $cleanExtensions;
343
- sort($sortedExtensions);
344
- $extensionsHex = array_map(fn($e) => sprintf('%04x', $e), $sortedExtensions);
345
- $extensionsString = implode(',', $extensionsHex);
346
-
347
- $sortedSigAlgs = $cleanSigAlgs;
348
- sort($sortedSigAlgs);
349
- $sigAlgsHex = array_map(fn($s) => sprintf('%04x', $s), $sortedSigAlgs);
350
- $sigAlgsString = implode(',', $sigAlgsHex);
351
-
352
- $ja4_c_input = $extensionsString . '_' . $sigAlgsString;
353
- $ja4_c = substr(hash('sha256', $ja4_c_input), 0, 12);
354
-
355
- $ja4_hash = $ja4_a . "_" . $ja4_b . "_" . $ja4_c;
356
-
357
- $result = [
358
- 'ja3_string' => $ja3String,
359
- 'ja3_hash' => md5($ja3String),
360
- 'ja4_raw' => $ja4_hash
361
- ];
362
-
363
- if ($quicParams !== null) {
364
- $result['quic_params'] = $quicParams;
365
- $result['quic_fp'] = self::formatQuicFingerprint($quicParams);
366
- }
367
-
368
- return $result;
369
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Utils;
6
+
7
+ /**
8
+ * Décode nativement un paquet TLS Client Hello binaire pour calculer l'empreinte JA3/JA4.
9
+ */
10
+ class TLSClientHelloParser
11
+ {
12
+ private const GREASE_VALUES = [
13
+ 2570, 6682, 10794, 14906, 19018, 23130, 27242, 31354,
14
+ 35466, 39578, 43690, 47802, 51914, 55926, 60038, 64150
15
+ ];
16
+
17
+ /**
18
+ * Lit un entier encodé en longueur variable QUIC (RFC 9000 section 16).
19
+ */
20
+ public static function readVarInt(string $data, int &$offset): ?int
21
+ {
22
+ $len = strlen($data);
23
+ if ($offset >= $len) {
24
+ return null;
25
+ }
26
+
27
+ $first = ord($data[$offset]);
28
+ $prefix = $first >> 6;
29
+ $firstVal = $first & 0x3f;
30
+
31
+ if ($prefix === 0) {
32
+ $offset += 1;
33
+ return $firstVal;
34
+ } elseif ($prefix === 1) {
35
+ if ($offset + 2 > $len) return null;
36
+ $val = ($firstVal << 8) | ord($data[$offset + 1]);
37
+ $offset += 2;
38
+ return $val;
39
+ } elseif ($prefix === 2) {
40
+ if ($offset + 4 > $len) return null;
41
+ $val = ($firstVal << 24) | (ord($data[$offset + 1]) << 16) | (ord($data[$offset + 2]) << 8) | ord($data[$offset + 3]);
42
+ $offset += 4;
43
+ return $val;
44
+ } else {
45
+ if ($offset + 8 > $len) return null;
46
+ $val = $firstVal;
47
+ for ($i = 1; $i < 8; $i++) {
48
+ $val = ($val << 8) | ord($data[$offset + $i]);
49
+ }
50
+ $offset += 8;
51
+ return $val;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Parse les paramètres de transport QUIC (RFC 9000 section 18.2).
57
+ */
58
+ public static function parseQuicTransportParameters(string $data): array
59
+ {
60
+ $params = [];
61
+ $offset = 0;
62
+ $len = strlen($data);
63
+
64
+ while ($offset < $len) {
65
+ $paramId = self::readVarInt($data, $offset);
66
+ if ($paramId === null) break;
67
+ $paramLen = self::readVarInt($data, $offset);
68
+ if ($paramLen === null || $offset + $paramLen > $len) break;
69
+
70
+ $paramValBytes = substr($data, $offset, $paramLen);
71
+ $offset += $paramLen;
72
+
73
+ if ($paramLen > 0 && in_array($paramId, [1, 3, 4, 5, 6, 7, 8, 9, 11, 14], true)) {
74
+ $valOffset = 0;
75
+ $numVal = self::readVarInt($paramValBytes, $valOffset);
76
+ $params[$paramId] = $numVal ?? $paramValBytes;
77
+ } else {
78
+ $params[$paramId] = bin2hex($paramValBytes);
79
+ }
80
+ }
81
+
82
+ return $params;
83
+ }
84
+
85
+ /**
86
+ * Analyse l'ordre des trames de contrôle QUIC / HTTP/3 (SETTINGS, MAX_STREAMS, PRIORITY).
87
+ */
88
+ public static function parseQuicControlFrames(string $streamData): array
89
+ {
90
+ $frames = [];
91
+ $frameOrder = [];
92
+ $settings = [];
93
+ $priorityUpdates = [];
94
+ $offset = 0;
95
+ $len = strlen($streamData);
96
+
97
+ if ($len === 0) {
98
+ return ['frames' => [], 'frame_order' => '', 'settings' => []];
99
+ }
100
+
101
+ if (ord($streamData[0]) === 0x00) {
102
+ $offset = 1; // Stream type Control Stream
103
+ }
104
+
105
+ while ($offset < $len) {
106
+ $frameType = self::readVarInt($streamData, $offset);
107
+ if ($frameType === null) break;
108
+ $frameLen = self::readVarInt($streamData, $offset);
109
+ if ($frameLen === null || $offset + $frameLen > $len) break;
110
+
111
+ $payload = substr($streamData, $offset, $frameLen);
112
+ $offset += $frameLen;
113
+
114
+ $abbr = match ($frameType) {
115
+ 0x04 => 's', // SETTINGS
116
+ 0x12, 0x02 => 'm', // MAX_STREAMS
117
+ 0x0f, 0xaf, 0xf0700 => 'p', // PRIORITY_UPDATE
118
+ 0x10, 0x0d => 'd', // MAX_DATA
119
+ 0x07 => 'g', // GOAWAY
120
+ default => 'u'
121
+ };
122
+
123
+ $frames[] = ['type' => $frameType, 'length' => $frameLen];
124
+ $frameOrder[] = $abbr;
125
+
126
+ if ($frameType === 0x04) {
127
+ $sOffset = 0;
128
+ $sLen = strlen($payload);
129
+ while ($sOffset < $sLen) {
130
+ $sId = self::readVarInt($payload, $sOffset);
131
+ if ($sId === null) break;
132
+ $sVal = self::readVarInt($payload, $sOffset);
133
+ if ($sVal === null) break;
134
+ $settings[$sId] = $sVal;
135
+ }
136
+ }
137
+ if ($frameType === 0x0f || $frameType === 0xaf || $frameType === 0xf0700) {
138
+ $pOffset = 0;
139
+ $prioritizedId = self::readVarInt($payload, $pOffset);
140
+ $priorityString = ($pOffset !== null && $pOffset < strlen($payload)) ? substr($payload, $pOffset) : '';
141
+ $priorityUpdates[] = [
142
+ 'stream_id' => $prioritizedId,
143
+ 'priority' => $priorityString
144
+ ];
145
+ }
146
+ }
147
+
148
+ return [
149
+ 'frames' => $frames,
150
+ 'frame_order' => implode(',', $frameOrder),
151
+ 'settings' => $settings,
152
+ 'priority_updates' => $priorityUpdates
153
+ ];
154
+ }
155
+
156
+ public static function formatQuicFingerprint(array $params, string $priority = '', string $frameOrder = ''): string
157
+ {
158
+ $paramParts = [];
159
+ foreach ($params as $k => $v) {
160
+ $paramParts[] = "{$k}={$v}";
161
+ }
162
+ $fp = "1;" . implode(',', $paramParts);
163
+ if ($priority !== '' || $frameOrder !== '') {
164
+ $fp .= ";{$priority}";
165
+ }
166
+ if ($frameOrder !== '') {
167
+ $fp .= ";{$frameOrder}";
168
+ }
169
+ return $fp;
170
+ }
171
+
172
+ /**
173
+ * Parse le Client Hello brut et retourne l'empreinte JA3 et une approximation JA4.
174
+ *
175
+ * @param string $binary Le premier paquet TCP reçu sur la socket.
176
+ * @return array{ja3_string: string, ja3_hash: string, ja4_raw: string}|null
177
+ */
178
+ public static function parse(string $binary): ?array
179
+ {
180
+ $len = strlen($binary);
181
+ if ($len < 43) {
182
+ return null; // Paquet trop court
183
+ }
184
+
185
+ // 1. Vérification du Record Layer Type (0x16 = Handshake)
186
+ if (ord($binary[0]) !== 0x16) {
187
+ return null;
188
+ }
189
+
190
+ // 2. Vérification du Handshake Type (0x01 = Client Hello)
191
+ if (ord($binary[5]) !== 0x01) {
192
+ return null;
193
+ }
194
+
195
+ $offset = 43; // Sauter l'en-tête, la version et le Random Client (32 octets)
196
+ if ($len < $offset + 1) return null;
197
+
198
+ // 3. Lecture du Session ID
199
+ $sessionLen = ord($binary[$offset]);
200
+ $offset += 1 + $sessionLen;
201
+ if ($len < $offset + 2) return null;
202
+
203
+ // 4. Lecture des Cipher Suites
204
+ $ciphersLen = unpack('n', substr($binary, $offset, 2))[1];
205
+ $offset += 2;
206
+ if ($len < $offset + $ciphersLen + 1) return null;
207
+
208
+ $ciphers = [];
209
+ for ($i = 0; $i < $ciphersLen; $i += 2) {
210
+ $ciphers[] = unpack('n', substr($binary, $offset + $i, 2))[1];
211
+ }
212
+ $offset += $ciphersLen;
213
+
214
+ // 5. Lecture des Compression Methods
215
+ $compressionLen = ord($binary[$offset]);
216
+ $offset += 1 + $compressionLen;
217
+ if ($len < $offset + 2) return null;
218
+
219
+ // 6. Lecture des Extensions
220
+ $extensionsLen = unpack('n', substr($binary, $offset, 2))[1];
221
+ $offset += 2;
222
+
223
+ $extensions = [];
224
+ $curves = [];
225
+ $points = [];
226
+ $sigAlgs = [];
227
+ $supportedVersions = [];
228
+ $hasEch = false;
229
+ $hasSni = false;
230
+ $alpnProtocol = '';
231
+ $quicParams = null;
232
+
233
+ $extLimit = $offset + $extensionsLen;
234
+ while ($offset < $extLimit && $offset + 4 <= $len) {
235
+ $extType = unpack('n', substr($binary, $offset, 2))[1];
236
+ $extLen = unpack('n', substr($binary, $offset + 2, 2))[1];
237
+ $offset += 4;
238
+
239
+ if ($offset + $extLen > $len) break;
240
+
241
+ $extensions[] = $extType;
242
+
243
+ if ($extType === 0) {
244
+ $hasSni = true;
245
+ } elseif ($extType === 0xfe0d) { // encrypted_client_hello (RFC 9460)
246
+ $hasEch = true;
247
+ } elseif ($extType === 10) { // Extension Supported Groups (Elliptic Curves)
248
+ if ($extLen >= 2) {
249
+ $curvesLen = unpack('n', substr($binary, $offset, 2))[1];
250
+ for ($j = 2; $j < $curvesLen + 2; $j += 2) {
251
+ $curves[] = unpack('n', substr($binary, $offset + $j, 2))[1];
252
+ }
253
+ }
254
+ } elseif ($extType === 11) { // Extension EC Point Formats
255
+ if ($extLen >= 1) {
256
+ $pointsLen = ord($binary[$offset]);
257
+ for ($j = 1; $j < $pointsLen + 1; $j++) {
258
+ $points[] = ord($binary[$offset + $j]);
259
+ }
260
+ }
261
+ } elseif ($extType === 13) { // Signature Algorithms
262
+ if ($extLen >= 2 && $offset + 2 <= $len) {
263
+ $sigAlgsLen = unpack('n', substr($binary, $offset, 2))[1];
264
+ for ($j = 2; $j < $sigAlgsLen + 2; $j += 2) {
265
+ if ($offset + $j + 2 <= $len && $j + 2 <= $extLen) {
266
+ $sigAlgs[] = unpack('n', substr($binary, $offset + $j, 2))[1];
267
+ }
268
+ }
269
+ }
270
+ } elseif ($extType === 16) { // ALPN
271
+ if ($extLen >= 3 && $offset + 2 <= $len) {
272
+ $alpnListLen = unpack('n', substr($binary, $offset, 2))[1];
273
+ if ($extLen >= 2 + $alpnListLen && $offset + 2 + $alpnListLen <= $len) {
274
+ $alpnStrLen = ord($binary[$offset + 2]);
275
+ if ($alpnListLen >= 1 + $alpnStrLen) {
276
+ $alpnProtocol = substr($binary, $offset + 3, $alpnStrLen);
277
+ }
278
+ }
279
+ }
280
+ } elseif ($extType === 43) { // Supported Versions
281
+ if ($extLen >= 1 && $offset + 1 <= $len) {
282
+ $versionsLen = ord($binary[$offset]);
283
+ for ($j = 1; $j < $versionsLen + 1; $j += 2) {
284
+ if ($offset + $j + 2 <= $len && $j + 2 <= $extLen) {
285
+ $supportedVersions[] = unpack('n', substr($binary, $offset + $j, 2))[1];
286
+ }
287
+ }
288
+ }
289
+ } elseif ($extType === 57 || $extType === 0xffa5) { // quic_transport_parameters (RFC 9001 / draft)
290
+ if ($extLen > 0 && $offset + $extLen <= $len) {
291
+ $quicData = substr($binary, $offset, $extLen);
292
+ $quicParams = self::parseQuicTransportParameters($quicData);
293
+ }
294
+ }
295
+ $offset += $extLen;
296
+ }
297
+
298
+ // Nettoyage des valeurs GREASE (RFC 8701) pour la conformité JA3
299
+ $filterGrease = fn(array $arr) => array_values(array_filter($arr, fn($v) => !in_array($v, self::GREASE_VALUES, true)));
300
+
301
+ $cleanCiphers = $filterGrease($ciphers);
302
+ $cleanExtensions = $filterGrease($extensions);
303
+ $cleanCurves = $filterGrease($curves);
304
+ $cleanPoints = $filterGrease($points);
305
+ $cleanSigAlgs = $filterGrease($sigAlgs);
306
+ $cleanSupportedVersions = $filterGrease($supportedVersions);
307
+
308
+ $sslVersion = unpack('n', substr($binary, 9, 2))[1];
309
+ $ja3String = implode(',', [
310
+ $sslVersion,
311
+ implode('-', $cleanCiphers),
312
+ implode('-', $cleanExtensions),
313
+ implode('-', $cleanCurves),
314
+ implode('-', $cleanPoints)
315
+ ]);
316
+
317
+ // Calcul natif de JA4
318
+ $highestVersion = $sslVersion;
319
+ if (!empty($cleanSupportedVersions)) {
320
+ $highestVersion = max($cleanSupportedVersions);
321
+ }
322
+
323
+ $ja4Version = "12";
324
+ if ($highestVersion === 0x0304) {
325
+ $ja4Version = "13";
326
+ } elseif ($highestVersion === 0x0303) {
327
+ $ja4Version = "12";
328
+ } elseif ($highestVersion === 0x0302) {
329
+ $ja4Version = "11";
330
+ } elseif ($highestVersion === 0x0301) {
331
+ $ja4Version = "10";
332
+ }
333
+
334
+ $sniStatus = $hasSni ? "d" : "i";
335
+ $numCiphers = min(99, count($cleanCiphers));
336
+ $numExtensions = min(99, count($cleanExtensions));
337
+
338
+ $ja4Alpn = "00";
339
+ if ($alpnProtocol !== '') {
340
+ $lenAlpn = strlen($alpnProtocol);
341
+ if ($lenAlpn === 1) {
342
+ $ja4Alpn = $alpnProtocol . $alpnProtocol;
343
+ } else {
344
+ $ja4Alpn = $alpnProtocol[0] . $alpnProtocol[$lenAlpn - 1];
345
+ }
346
+ }
347
+
348
+ $ja4_a = "t" . $ja4Version . $sniStatus . sprintf("%02d", $numCiphers) . sprintf("%02d", $numExtensions) . $ja4Alpn;
349
+
350
+ $sortedCiphers = $cleanCiphers;
351
+ sort($sortedCiphers);
352
+ $ciphersHex = array_map(fn($c) => sprintf('%04x', $c), $sortedCiphers);
353
+ $ciphersString = implode(',', $ciphersHex);
354
+ $ja4_b = substr(hash('sha256', $ciphersString), 0, 12);
355
+
356
+ $sortedExtensions = $cleanExtensions;
357
+ sort($sortedExtensions);
358
+ $extensionsHex = array_map(fn($e) => sprintf('%04x', $e), $sortedExtensions);
359
+ $extensionsString = implode(',', $extensionsHex);
360
+
361
+ $sortedSigAlgs = $cleanSigAlgs;
362
+ sort($sortedSigAlgs);
363
+ $sigAlgsHex = array_map(fn($s) => sprintf('%04x', $s), $sortedSigAlgs);
364
+ $sigAlgsString = implode(',', $sigAlgsHex);
365
+
366
+ $ja4_c_input = $extensionsString . '_' . $sigAlgsString;
367
+ $ja4_c = substr(hash('sha256', $ja4_c_input), 0, 12);
368
+
369
+ $ja4_hash = $ja4_a . "_" . $ja4_b . "_" . $ja4_c;
370
+
371
+ $result = [
372
+ 'ja3_string' => $ja3String,
373
+ 'ja3_hash' => md5($ja3String),
374
+ 'ja4_raw' => $ja4_hash,
375
+ 'has_ech' => $hasEch,
376
+ 'has_sni' => $hasSni
377
+ ];
378
+
379
+ if ($quicParams !== null) {
380
+ $result['quic_params'] = $quicParams;
381
+ $result['quic_fp'] = self::formatQuicFingerprint($quicParams);
382
+ }
383
+
384
+ return $result;
385
+ }
370
386
  }