@anonympins/fingerprint 0.4.3 → 0.4.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 +21 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/js/fingerprint.client.js +831 -635
- package/src/js/fingerprint.js +239 -29
- package/src/js/tests/fingerprint.client.init.test.js +140 -119
- package/src/js/tests/fingerprint.test.js +110 -10
- package/src/php/Challenge/ChallengeUtils.php +415 -361
- package/src/php/Config/SecurityProfiles.php +276 -271
- package/src/php/FingerprintClient.php +131 -131
- package/src/php/FingerprintEngine.php +26 -0
- package/src/php/RequestContext.php +90 -90
- package/src/php/Store/IStore.php +41 -41
- package/src/php/Store/RedisStore.php +53 -53
- package/src/php/Tests/RequestUtilsTest.php +130 -0
- package/src/php/Utils/RequestUtils.php +200 -16
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint;
|
|
6
|
-
use Anonympins\Fingerprint\Config\SecurityProfiles;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* FingerprintClient - Wrapper PHP pour la bibliothèque de fingerprinting côté client.
|
|
10
|
-
*
|
|
11
|
-
* Cette classe facilite l'intégration de la bibliothèque JavaScript `fingerprint.client.js`
|
|
12
|
-
* dans une application PHP. Elle gère l'injection sécurisée du script et la création
|
|
13
|
-
* de "honeypots" (pièges à bots) dans les formulaires.
|
|
14
|
-
*/
|
|
15
|
-
class FingerprintClient
|
|
16
|
-
{
|
|
17
|
-
/**
|
|
18
|
-
* @var string Le chemin vers le fichier de la bibliothèque client JavaScript.
|
|
19
|
-
*/
|
|
20
|
-
private string $clientScriptPath;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @var array La configuration à passer à la fonction `initializeClient` de la bibliothèque JS.
|
|
24
|
-
*/
|
|
25
|
-
private array $clientConfig;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @var string|null Un nonce cryptographique pour la Content Security Policy (CSP).
|
|
29
|
-
*/
|
|
30
|
-
private ?string $nonce;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Constructeur de la classe.
|
|
34
|
-
*
|
|
35
|
-
* @param string $clientScriptPath Le chemin d'accès web au fichier `fingerprint.client.js`.
|
|
36
|
-
* @param array $clientConfig La configuration pour la bibliothèque client (souris, frappes, honeypots, etc.).
|
|
37
|
-
*/
|
|
38
|
-
public function __construct(string $clientScriptPath, array $clientConfig = [])
|
|
39
|
-
{
|
|
40
|
-
$this->clientScriptPath = $clientScriptPath;
|
|
41
|
-
|
|
42
|
-
$defaultConfig = [
|
|
43
|
-
'mouse' => true,
|
|
44
|
-
'keystrokes' => true,
|
|
45
|
-
'clicks' => true,
|
|
46
|
-
'honeypots' => [],
|
|
47
|
-
'fetch' => [
|
|
48
|
-
'handleChallenges' => true,
|
|
49
|
-
'probationaryTtl' => 30000, // 30 seconds
|
|
50
|
-
],
|
|
51
|
-
'wasm' => true, // Activer la tentative de chargement du module WASM
|
|
52
|
-
'wasmPath' => '/fp.js' // Chemin vers le script de chargement WASM
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
// Utiliser une fusion profonde pour permettre de surcharger des sous-clés
|
|
56
|
-
$this->clientConfig = SecurityProfiles::deepMerge($defaultConfig, $clientConfig);
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
// Génère un nonce pour CSP si possible, pour une sécurité renforcée.
|
|
60
|
-
$this->nonce = bin2hex(random_bytes(16));
|
|
61
|
-
} catch (\Exception $e) {
|
|
62
|
-
$this->nonce = null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Génère un champ de formulaire "honeypot" caché.
|
|
68
|
-
* Les bots le rempliront, mais il sera invisible pour les humains.
|
|
69
|
-
*
|
|
70
|
-
* @param string $fieldName Le nom du champ (doit correspondre à la configuration client).
|
|
71
|
-
* @return string Le code HTML du champ honeypot.
|
|
72
|
-
*/
|
|
73
|
-
public function generateHoneypotField(string $fieldName): string
|
|
74
|
-
{
|
|
75
|
-
// Ajoute le champ à la configuration pour que le script client le surveille.
|
|
76
|
-
if (!in_array($fieldName, $this->clientConfig['honeypots'])) {
|
|
77
|
-
$this->clientConfig['honeypots'][] = $fieldName;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Styles CSS pour cacher le champ de manière robuste.
|
|
81
|
-
$styles = 'position:absolute; left:-9999px; top:-9999px; opacity:0;';
|
|
82
|
-
|
|
83
|
-
return '<div style="' . $styles . '" aria-hidden="true">'
|
|
84
|
-
. '<label for="' . htmlspecialchars($fieldName) . '">
|
|
85
|
-
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
86
|
-
. '</div>';
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Génère le bloc de script complet à inclure dans une page HTML.
|
|
91
|
-
*
|
|
92
|
-
* @return string Le code HTML des balises <script>.
|
|
93
|
-
*/
|
|
94
|
-
public function getScriptTag(): string
|
|
95
|
-
{
|
|
96
|
-
$configJson = json_encode($this->clientConfig);
|
|
97
|
-
$nonceAttr = $this->nonce ? ' nonce="' . $this->nonce . '"' : '';
|
|
98
|
-
|
|
99
|
-
// Le script d'initialisation qui sera inclus dans la page.
|
|
100
|
-
$initScript = <<<JS
|
|
101
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
102
|
-
const config = {$configJson};
|
|
103
|
-
if (window.ClientLibrary) {
|
|
104
|
-
if (config.wasmPath) {
|
|
105
|
-
const wasmScript = document.createElement('script');
|
|
106
|
-
wasmScript.src = config.wasmPath;
|
|
107
|
-
wasmScript.async = true;
|
|
108
|
-
wasmScript.nonce = '{$this->nonce}';
|
|
109
|
-
document.head.appendChild(wasmScript);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
window.ClientLibrary.initializeClient(config);
|
|
113
|
-
} else {
|
|
114
|
-
console.error('Fingerprint client library not loaded.');
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
JS;
|
|
118
|
-
|
|
119
|
-
// On combine le chargement de la bibliothèque et le script d'initialisation.
|
|
120
|
-
return '<script src="' . htmlspecialchars($this->clientScriptPath) . '"' . $nonceAttr . '></script>'
|
|
121
|
-
. '<script' . $nonceAttr . '>' . $initScript . '</script>';
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Retourne le nonce généré pour pouvoir l'utiliser dans les en-têtes CSP.
|
|
126
|
-
* @return string|null
|
|
127
|
-
*/
|
|
128
|
-
public function getNonce(): ?string
|
|
129
|
-
{
|
|
130
|
-
return $this->nonce;
|
|
131
|
-
}
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
use Anonympins\Fingerprint\Config\SecurityProfiles;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* FingerprintClient - Wrapper PHP pour la bibliothèque de fingerprinting côté client.
|
|
10
|
+
*
|
|
11
|
+
* Cette classe facilite l'intégration de la bibliothèque JavaScript `fingerprint.client.js`
|
|
12
|
+
* dans une application PHP. Elle gère l'injection sécurisée du script et la création
|
|
13
|
+
* de "honeypots" (pièges à bots) dans les formulaires.
|
|
14
|
+
*/
|
|
15
|
+
class FingerprintClient
|
|
16
|
+
{
|
|
17
|
+
/**
|
|
18
|
+
* @var string Le chemin vers le fichier de la bibliothèque client JavaScript.
|
|
19
|
+
*/
|
|
20
|
+
private string $clientScriptPath;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @var array La configuration à passer à la fonction `initializeClient` de la bibliothèque JS.
|
|
24
|
+
*/
|
|
25
|
+
private array $clientConfig;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @var string|null Un nonce cryptographique pour la Content Security Policy (CSP).
|
|
29
|
+
*/
|
|
30
|
+
private ?string $nonce;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Constructeur de la classe.
|
|
34
|
+
*
|
|
35
|
+
* @param string $clientScriptPath Le chemin d'accès web au fichier `fingerprint.client.js`.
|
|
36
|
+
* @param array $clientConfig La configuration pour la bibliothèque client (souris, frappes, honeypots, etc.).
|
|
37
|
+
*/
|
|
38
|
+
public function __construct(string $clientScriptPath, array $clientConfig = [])
|
|
39
|
+
{
|
|
40
|
+
$this->clientScriptPath = $clientScriptPath;
|
|
41
|
+
|
|
42
|
+
$defaultConfig = [
|
|
43
|
+
'mouse' => true,
|
|
44
|
+
'keystrokes' => true,
|
|
45
|
+
'clicks' => true,
|
|
46
|
+
'honeypots' => [],
|
|
47
|
+
'fetch' => [
|
|
48
|
+
'handleChallenges' => true,
|
|
49
|
+
'probationaryTtl' => 30000, // 30 seconds
|
|
50
|
+
],
|
|
51
|
+
'wasm' => true, // Activer la tentative de chargement du module WASM
|
|
52
|
+
'wasmPath' => '/fp.js' // Chemin vers le script de chargement WASM
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// Utiliser une fusion profonde pour permettre de surcharger des sous-clés
|
|
56
|
+
$this->clientConfig = SecurityProfiles::deepMerge($defaultConfig, $clientConfig);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Génère un nonce pour CSP si possible, pour une sécurité renforcée.
|
|
60
|
+
$this->nonce = bin2hex(random_bytes(16));
|
|
61
|
+
} catch (\Exception $e) {
|
|
62
|
+
$this->nonce = null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Génère un champ de formulaire "honeypot" caché.
|
|
68
|
+
* Les bots le rempliront, mais il sera invisible pour les humains.
|
|
69
|
+
*
|
|
70
|
+
* @param string $fieldName Le nom du champ (doit correspondre à la configuration client).
|
|
71
|
+
* @return string Le code HTML du champ honeypot.
|
|
72
|
+
*/
|
|
73
|
+
public function generateHoneypotField(string $fieldName): string
|
|
74
|
+
{
|
|
75
|
+
// Ajoute le champ à la configuration pour que le script client le surveille.
|
|
76
|
+
if (!in_array($fieldName, $this->clientConfig['honeypots'])) {
|
|
77
|
+
$this->clientConfig['honeypots'][] = $fieldName;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Styles CSS pour cacher le champ de manière robuste.
|
|
81
|
+
$styles = 'position:absolute; left:-9999px; top:-9999px; transform:scale(0); opacity:0; pointer-events:none;';
|
|
82
|
+
|
|
83
|
+
return '<div style="' . $styles . '" aria-hidden="true">'
|
|
84
|
+
. '<label for="' . htmlspecialchars($fieldName) . '">' . $fieldName . '</label>'
|
|
85
|
+
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
86
|
+
. '</div>';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Génère le bloc de script complet à inclure dans une page HTML.
|
|
91
|
+
*
|
|
92
|
+
* @return string Le code HTML des balises <script>.
|
|
93
|
+
*/
|
|
94
|
+
public function getScriptTag(): string
|
|
95
|
+
{
|
|
96
|
+
$configJson = json_encode($this->clientConfig);
|
|
97
|
+
$nonceAttr = $this->nonce ? ' nonce="' . $this->nonce . '"' : '';
|
|
98
|
+
|
|
99
|
+
// Le script d'initialisation qui sera inclus dans la page.
|
|
100
|
+
$initScript = <<<JS
|
|
101
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
102
|
+
const config = {$configJson};
|
|
103
|
+
if (window.ClientLibrary) {
|
|
104
|
+
if (config.wasmPath) {
|
|
105
|
+
const wasmScript = document.createElement('script');
|
|
106
|
+
wasmScript.src = config.wasmPath;
|
|
107
|
+
wasmScript.async = true;
|
|
108
|
+
wasmScript.nonce = '{$this->nonce}';
|
|
109
|
+
document.head.appendChild(wasmScript);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
window.ClientLibrary.initializeClient(config);
|
|
113
|
+
} else {
|
|
114
|
+
console.error('Fingerprint client library not loaded.');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
JS;
|
|
118
|
+
|
|
119
|
+
// On combine le chargement de la bibliothèque et le script d'initialisation.
|
|
120
|
+
return '<script src="' . htmlspecialchars($this->clientScriptPath) . '"' . $nonceAttr . '></script>'
|
|
121
|
+
. '<script' . $nonceAttr . '>' . $initScript . '</script>';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Retourne le nonce généré pour pouvoir l'utiliser dans les en-têtes CSP.
|
|
126
|
+
* @return string|null
|
|
127
|
+
*/
|
|
128
|
+
public function getNonce(): ?string
|
|
129
|
+
{
|
|
130
|
+
return $this->nonce;
|
|
131
|
+
}
|
|
132
132
|
}
|
|
@@ -417,6 +417,11 @@
|
|
|
417
417
|
// Score d'incohérence des Client-Hints
|
|
418
418
|
$clientHintsInconsistency = RequestUtils::getClientHintsInconsistencyScore($context);
|
|
419
419
|
|
|
420
|
+
// Score de similarité globale de l'empreinte (Clustering Botnet)
|
|
421
|
+
$stableFp = RequestUtils::extractStablePart($currentDeviceHash);
|
|
422
|
+
$stableFpHash = FingerprintBuilder::cyrb53($stableFp);
|
|
423
|
+
$botnetCluster = RequestUtils::getBotnetClusterScore($context, $stableFpHash);
|
|
424
|
+
|
|
420
425
|
// NOUVEAU: Score de réputation du sous-réseau IP
|
|
421
426
|
$subnetScore = RequestUtils::getSubnetScore($context, $deviceId);
|
|
422
427
|
|
|
@@ -437,6 +442,7 @@
|
|
|
437
442
|
'threatIntelScore' => $threatIntel['threatIntelScore'],
|
|
438
443
|
'clientHintsInconsistencyScore' => $clientHintsInconsistency['clientHintsInconsistencyScore'],
|
|
439
444
|
'subnetScore' => $subnetScore['subnetScore'],
|
|
445
|
+
'botnetClusterScore' => $botnetCluster['botnetClusterScore'],
|
|
440
446
|
]);
|
|
441
447
|
|
|
442
448
|
// Sauvegarder l'état mis à jour de l'appareil dans le store
|
|
@@ -745,6 +751,26 @@
|
|
|
745
751
|
$this->log('High suspicion score detected - overriding valid ticket to re-issue challenge', ['finalScore' => $finalScore, 'deviceId' => $deviceId]);
|
|
746
752
|
}
|
|
747
753
|
|
|
754
|
+
// --- AJOUT: Limiteur de débit (Token Bucket) ---
|
|
755
|
+
$rateLimitPassed = ChallengeUtils::checkChallengeRateLimit($context->clientIp);
|
|
756
|
+
if (!$rateLimitPassed) {
|
|
757
|
+
$this->log('Challenge rate limit exceeded - blocking with 429', ['clientIp' => $context->clientIp]);
|
|
758
|
+
$decision = [
|
|
759
|
+
'action' => 'block',
|
|
760
|
+
'status' => 429,
|
|
761
|
+
'body' => 'Too Many Requests',
|
|
762
|
+
'score' => $finalScore,
|
|
763
|
+
'vector' => $suspicionVector
|
|
764
|
+
];
|
|
765
|
+
if ($this->dryRun) {
|
|
766
|
+
$this->log("[Dry Run] Intended action: {$decision['action']}", ['score' => $decision['score']]);
|
|
767
|
+
$decision['intendedAction'] = $decision['action'];
|
|
768
|
+
$decision['action'] = 'next';
|
|
769
|
+
unset($decision['status'], $decision['body']);
|
|
770
|
+
}
|
|
771
|
+
return $decision;
|
|
772
|
+
}
|
|
773
|
+
|
|
748
774
|
$decision = ['action' => 'challenge', 'score' => $finalScore, 'vector' => $suspicionVector, 'status' => 403];
|
|
749
775
|
|
|
750
776
|
if ($this->dryRun) {
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Représente le contexte d'une requête HTTP, fournissant un accès unifié
|
|
9
|
-
* aux informations nécessaires pour l'analyse de l'empreinte.
|
|
10
|
-
* Cette classe est conçue pour être créée à partir d'un objet de requête
|
|
11
|
-
* standard (ex: PSR-7, Symfony, Laravel).
|
|
12
|
-
*/
|
|
13
|
-
class RequestContext
|
|
14
|
-
{
|
|
15
|
-
public string $clientIp;
|
|
16
|
-
public string $path;
|
|
17
|
-
/** @var array<string, string> */
|
|
18
|
-
public array $headers;
|
|
19
|
-
/** @var array<string, mixed> */
|
|
20
|
-
public array $query;
|
|
21
|
-
/** @var array<string, mixed>|object|null */
|
|
22
|
-
public $body;
|
|
23
|
-
/** @var array<string, string> */
|
|
24
|
-
public array $cookies;
|
|
25
|
-
public ?string $httpVersion;
|
|
26
|
-
public int $requestTimestamp;
|
|
27
|
-
|
|
28
|
-
/** @var ?array{type: string, name: string} */
|
|
29
|
-
public ?array $graphqlOperation = null;
|
|
30
|
-
|
|
31
|
-
/** @var ?array<string, mixed> */
|
|
32
|
-
public ?array $newCookieForResponse = null;
|
|
33
|
-
|
|
34
|
-
// Propriétés spécifiques qui peuvent être fournies par un proxy inverse
|
|
35
|
-
public ?string $ja3;
|
|
36
|
-
public ?string $ja4;
|
|
37
|
-
public ?string $ja4s;
|
|
38
|
-
public ?string $ja4h;
|
|
39
|
-
public ?string $http2Fingerprint;
|
|
40
|
-
public ?string $tcpFingerprint;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @param string $clientIp
|
|
44
|
-
* @param string $path
|
|
45
|
-
* @param array<string, string> $headers
|
|
46
|
-
* @param array<string, mixed> $query
|
|
47
|
-
* @param array<string, mixed>|object|null $body
|
|
48
|
-
* @param array<string, string> $cookies
|
|
49
|
-
* @param string|null $httpVersion
|
|
50
|
-
* @param int|null $requestTimestamp
|
|
51
|
-
*/
|
|
52
|
-
public function __construct(
|
|
53
|
-
string $clientIp,
|
|
54
|
-
string $path,
|
|
55
|
-
array $headers,
|
|
56
|
-
array $query,
|
|
57
|
-
$body,
|
|
58
|
-
array $cookies,
|
|
59
|
-
?string $httpVersion,
|
|
60
|
-
?int $requestTimestamp = null
|
|
61
|
-
) {
|
|
62
|
-
$this->clientIp = $clientIp;
|
|
63
|
-
$this->path = $path;
|
|
64
|
-
// Normaliser les en-têtes en minuscules pour un accès cohérent
|
|
65
|
-
$this->headers = array_change_key_case($headers, CASE_LOWER);
|
|
66
|
-
$this->query = $query;
|
|
67
|
-
$this->body = $body;
|
|
68
|
-
$this->cookies = $cookies;
|
|
69
|
-
$this->httpVersion = $httpVersion;
|
|
70
|
-
$this->requestTimestamp = $requestTimestamp ?? (int)(microtime(true) * 1000);
|
|
71
|
-
|
|
72
|
-
// Extraire les empreintes TLS/HTTP2/TCP si elles sont fournies par les en-têtes
|
|
73
|
-
$this->ja3 = $this->headers['x-ja3-hash'] ?? null;
|
|
74
|
-
$this->ja4 = $this->headers['x-ja4-hash'] ?? null;
|
|
75
|
-
$this->ja4s = $this->headers['x-ja4s-hash'] ?? null;
|
|
76
|
-
$this->ja4h = $this->headers['x-ja4h-hash'] ?? null;
|
|
77
|
-
$this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
|
|
78
|
-
$this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Récupère la valeur d'un en-tête HTTP de manière insensible à la casse.
|
|
82
|
-
*
|
|
83
|
-
* @param string $name Le nom de l'en-tête.
|
|
84
|
-
* @return string|null La valeur de l'en-tête ou null si non trouvé.
|
|
85
|
-
*/
|
|
86
|
-
|
|
87
|
-
public function getHeader(string $name): ?string
|
|
88
|
-
{
|
|
89
|
-
return $this->headers[strtolower($name)] ?? null;
|
|
90
|
-
}
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Représente le contexte d'une requête HTTP, fournissant un accès unifié
|
|
9
|
+
* aux informations nécessaires pour l'analyse de l'empreinte.
|
|
10
|
+
* Cette classe est conçue pour être créée à partir d'un objet de requête
|
|
11
|
+
* standard (ex: PSR-7, Symfony, Laravel).
|
|
12
|
+
*/
|
|
13
|
+
class RequestContext
|
|
14
|
+
{
|
|
15
|
+
public string $clientIp = '';
|
|
16
|
+
public string $path = '';
|
|
17
|
+
/** @var array<string, string> */
|
|
18
|
+
public array $headers = [];
|
|
19
|
+
/** @var array<string, mixed> */
|
|
20
|
+
public array $query = [];
|
|
21
|
+
/** @var array<string, mixed>|object|null */
|
|
22
|
+
public $body = null;
|
|
23
|
+
/** @var array<string, string> */
|
|
24
|
+
public array $cookies = [];
|
|
25
|
+
public ?string $httpVersion = null;
|
|
26
|
+
public int $requestTimestamp = 0;
|
|
27
|
+
|
|
28
|
+
/** @var ?array{type: string, name: string} */
|
|
29
|
+
public ?array $graphqlOperation = null;
|
|
30
|
+
|
|
31
|
+
/** @var ?array<string, mixed> */
|
|
32
|
+
public ?array $newCookieForResponse = null;
|
|
33
|
+
|
|
34
|
+
// Propriétés spécifiques qui peuvent être fournies par un proxy inverse
|
|
35
|
+
public ?string $ja3 = null;
|
|
36
|
+
public ?string $ja4 = null;
|
|
37
|
+
public ?string $ja4s = null;
|
|
38
|
+
public ?string $ja4h = null;
|
|
39
|
+
public ?string $http2Fingerprint = null;
|
|
40
|
+
public ?string $tcpFingerprint = null;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param string $clientIp
|
|
44
|
+
* @param string $path
|
|
45
|
+
* @param array<string, string> $headers
|
|
46
|
+
* @param array<string, mixed> $query
|
|
47
|
+
* @param array<string, mixed>|object|null $body
|
|
48
|
+
* @param array<string, string> $cookies
|
|
49
|
+
* @param string|null $httpVersion
|
|
50
|
+
* @param int|null $requestTimestamp
|
|
51
|
+
*/
|
|
52
|
+
public function __construct(
|
|
53
|
+
string $clientIp,
|
|
54
|
+
string $path,
|
|
55
|
+
array $headers,
|
|
56
|
+
array $query,
|
|
57
|
+
$body,
|
|
58
|
+
array $cookies,
|
|
59
|
+
?string $httpVersion,
|
|
60
|
+
?int $requestTimestamp = null
|
|
61
|
+
) {
|
|
62
|
+
$this->clientIp = $clientIp;
|
|
63
|
+
$this->path = $path;
|
|
64
|
+
// Normaliser les en-têtes en minuscules pour un accès cohérent
|
|
65
|
+
$this->headers = array_change_key_case($headers, CASE_LOWER);
|
|
66
|
+
$this->query = $query;
|
|
67
|
+
$this->body = $body;
|
|
68
|
+
$this->cookies = $cookies;
|
|
69
|
+
$this->httpVersion = $httpVersion;
|
|
70
|
+
$this->requestTimestamp = $requestTimestamp ?? (int)(microtime(true) * 1000);
|
|
71
|
+
|
|
72
|
+
// Extraire les empreintes TLS/HTTP2/TCP si elles sont fournies par les en-têtes
|
|
73
|
+
$this->ja3 = $this->headers['x-ja3-hash'] ?? null;
|
|
74
|
+
$this->ja4 = $this->headers['x-ja4-hash'] ?? null;
|
|
75
|
+
$this->ja4s = $this->headers['x-ja4s-hash'] ?? null;
|
|
76
|
+
$this->ja4h = $this->headers['x-ja4h-hash'] ?? null;
|
|
77
|
+
$this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
|
|
78
|
+
$this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Récupère la valeur d'un en-tête HTTP de manière insensible à la casse.
|
|
82
|
+
*
|
|
83
|
+
* @param string $name Le nom de l'en-tête.
|
|
84
|
+
* @return string|null La valeur de l'en-tête ou null si non trouvé.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
public function getHeader(string $name): ?string
|
|
88
|
+
{
|
|
89
|
+
return $this->headers[strtolower($name)] ?? null;
|
|
90
|
+
}
|
|
91
91
|
}
|
package/src/php/Store/IStore.php
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint\Store;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Interface pour un système de stockage persistant.
|
|
9
|
-
* Utilisé pour stocker les données des appareils, les secrets des challenges, etc.
|
|
10
|
-
*/
|
|
11
|
-
interface IStore
|
|
12
|
-
{
|
|
13
|
-
/**
|
|
14
|
-
* Récupère une valeur associée à une clé.
|
|
15
|
-
* @param string $key
|
|
16
|
-
* @return mixed|null
|
|
17
|
-
*/
|
|
18
|
-
public function get(string $key);
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Stocke une valeur associée à une clé, avec une durée de vie optionnelle.
|
|
22
|
-
* @param string $key
|
|
23
|
-
* @param mixed $value
|
|
24
|
-
* @param int|null $ttl Durée de vie en secondes.
|
|
25
|
-
* @return void
|
|
26
|
-
*/
|
|
27
|
-
public function set(string $key, $value, ?int $ttl = null): void;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Vérifie si une clé existe dans le stockage.
|
|
31
|
-
* @param string $key
|
|
32
|
-
* @return bool
|
|
33
|
-
*/
|
|
34
|
-
public function has(string $key): bool;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Supprime une clé du stockage.
|
|
38
|
-
* @param string $key
|
|
39
|
-
* @return void
|
|
40
|
-
*/
|
|
41
|
-
public function delete(string $key): void;
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Store;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Interface pour un système de stockage persistant.
|
|
9
|
+
* Utilisé pour stocker les données des appareils, les secrets des challenges, etc.
|
|
10
|
+
*/
|
|
11
|
+
interface IStore
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* Récupère une valeur associée à une clé.
|
|
15
|
+
* @param string $key
|
|
16
|
+
* @return mixed|null
|
|
17
|
+
*/
|
|
18
|
+
public function get(string $key);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Stocke une valeur associée à une clé, avec une durée de vie optionnelle.
|
|
22
|
+
* @param string $key
|
|
23
|
+
* @param mixed $value
|
|
24
|
+
* @param int|null $ttl Durée de vie en secondes.
|
|
25
|
+
* @return void
|
|
26
|
+
*/
|
|
27
|
+
public function set(string $key, $value, ?int $ttl = null): void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Vérifie si une clé existe dans le stockage.
|
|
31
|
+
* @param string $key
|
|
32
|
+
* @return bool
|
|
33
|
+
*/
|
|
34
|
+
public function has(string $key): bool;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Supprime une clé du stockage.
|
|
38
|
+
* @param string $key
|
|
39
|
+
* @return void
|
|
40
|
+
*/
|
|
41
|
+
public function delete(string $key): void;
|
|
42
42
|
}
|