159 lines
6.3 KiB
PHP
159 lines
6.3 KiB
PHP
<?php
|
|
|
|
// Fonctions liées au verrouillage anti-bruteforce du code PIN du "Mode présentation"
|
|
// (voir pages/presentation.php). Le compteur de tentatives est stocké côté serveur, par
|
|
// adresse IP (hashée, jamais en clair), pour ne pas pouvoir être remis à zéro en vidant
|
|
// juste les cookies ou en ouvrant une fenêtre de navigation privée.
|
|
|
|
require_once(__DIR__ . '/databaseConnection.php');
|
|
|
|
// Nombre de chiffres attendu pour le code PIN
|
|
define('PRESENTATION_PIN_LENGTH', 9);
|
|
|
|
// Durée du blocage une fois le nombre maximum de tentatives atteint (en heures)
|
|
define('PRESENTATION_LOCK_HOURS', 24);
|
|
|
|
// Hash SHA-256 de l'IP du visiteur : sert de clé dans la table presentation_attempts.
|
|
// On ne stocke jamais l'IP en clair, un hash suffit pour reconnaître le même visiteur
|
|
// d'une requête à l'autre sans conserver une donnée personnelle en clair.
|
|
function hashIpPresentation() {
|
|
return hash('sha256', $_SERVER['REMOTE_ADDR'] ?? 'inconnue');
|
|
}
|
|
|
|
// Vérifie le code PIN saisi contre le hash enregistré dans les réglages du site.
|
|
// Renvoie false si aucun code n'a encore été configuré (accès bloqué par défaut tant
|
|
// que l'admin n'a pas défini de code depuis le panel).
|
|
function verifierPinPresentation($pinSaisi) {
|
|
require_once(__DIR__ . '/listSettings.php');
|
|
$hash = recupererSetting('presentation_pin_hash');
|
|
|
|
if (empty($hash) || strlen($pinSaisi) !== PRESENTATION_PIN_LENGTH) {
|
|
return false;
|
|
}
|
|
|
|
return password_verify($pinSaisi, $hash);
|
|
}
|
|
|
|
// Récupère l'état de blocage actuel pour une IP (hashée) donnée.
|
|
// Renvoie ['attempts' => int, 'locked_until' => string|null] ou null si cette IP n'a
|
|
// encore jamais raté de tentative.
|
|
function recupererTentativePresentation($ipHash) {
|
|
global $dbh;
|
|
|
|
try {
|
|
$stmt = $dbh->prepare('SELECT attempts, locked_until FROM presentation_attempts WHERE ip_hash = :ip');
|
|
$stmt->bindValue(':ip', $ipHash, PDO::PARAM_STR);
|
|
$stmt->execute();
|
|
$ligne = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
return $ligne ?: null;
|
|
} catch (PDOException $e) {
|
|
// Tant que la table n'existe pas encore (migration SQL pas encore appliquée),
|
|
// on considère qu'il n'y a pas de blocage plutôt que de casser la page
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Vrai si cette IP est actuellement bloquée (trop de tentatives ratées récemment)
|
|
function presentationEstVerrouillee($ipHash, $maxTentatives) {
|
|
$etat = recupererTentativePresentation($ipHash);
|
|
return $etat
|
|
&& (int) $etat['attempts'] >= $maxTentatives
|
|
&& !empty($etat['locked_until'])
|
|
&& strtotime($etat['locked_until']) > time();
|
|
}
|
|
|
|
// Enregistre une tentative ratée : incrémente le compteur de cette IP, et pose un
|
|
// blocage de PRESENTATION_LOCK_HOURS heures si le maximum est atteint.
|
|
// Renvoie le nouvel état, au même format que recupererTentativePresentation().
|
|
function enregistrerEchecPresentation($ipHash, $maxTentatives) {
|
|
global $dbh;
|
|
|
|
$etatActuel = recupererTentativePresentation($ipHash);
|
|
$tentatives = $etatActuel ? (int) $etatActuel['attempts'] + 1 : 1;
|
|
$verrouJusqua = $tentatives >= $maxTentatives
|
|
? date('Y-m-d H:i:s', time() + PRESENTATION_LOCK_HOURS * 3600)
|
|
: null;
|
|
|
|
try {
|
|
$stmt = $dbh->prepare(
|
|
'INSERT INTO presentation_attempts (ip_hash, attempts, locked_until) VALUES (:ip, :tentatives, :verrou)
|
|
ON DUPLICATE KEY UPDATE attempts = VALUES(attempts), locked_until = VALUES(locked_until)'
|
|
);
|
|
$stmt->execute([
|
|
':ip' => $ipHash,
|
|
':tentatives' => $tentatives,
|
|
':verrou' => $verrouJusqua
|
|
]);
|
|
} catch (PDOException $e) {
|
|
// Si la table n'existe pas encore, on ne bloque pas le visiteur pour autant
|
|
}
|
|
|
|
return ['attempts' => $tentatives, 'locked_until' => $verrouJusqua];
|
|
}
|
|
|
|
// Efface le compteur de tentatives d'une IP : code correct, connexion admin, ou
|
|
// déblocage manuel depuis le panel admin
|
|
function reinitialiserTentativesPresentation($ipHash) {
|
|
global $dbh;
|
|
|
|
try {
|
|
$stmt = $dbh->prepare('DELETE FROM presentation_attempts WHERE ip_hash = :ip');
|
|
$stmt->bindValue(':ip', $ipHash, PDO::PARAM_STR);
|
|
$stmt->execute();
|
|
} catch (PDOException $e) {
|
|
// Rien à faire si la table n'existe pas encore
|
|
}
|
|
}
|
|
|
|
// Liste des IP actuellement bloquées, pour le panel admin
|
|
function listerTentativesVerrouillees() {
|
|
global $dbh;
|
|
|
|
try {
|
|
$stmt = $dbh->query(
|
|
'SELECT ip_hash, attempts, locked_until FROM presentation_attempts
|
|
WHERE locked_until IS NOT NULL AND locked_until > NOW()
|
|
ORDER BY locked_until DESC'
|
|
);
|
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (PDOException $e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Affiche, dans le panel admin, la liste des IP actuellement bloquées avec un bouton
|
|
// de déblocage manuel pour chacune (utile si un visiteur légitime se fait bloquer)
|
|
function afficherTentativesVerrouilleesAdmin() {
|
|
$lignes = listerTentativesVerrouillees();
|
|
|
|
if (empty($lignes)) {
|
|
echo '<p class="admin-notice">Aucune IP bloquée actuellement.</p>';
|
|
return;
|
|
}
|
|
|
|
echo '<div class="projects-list">';
|
|
|
|
foreach ($lignes as $ligne) {
|
|
// On n'affiche jamais l'IP en clair : seulement une courte empreinte de son hash,
|
|
// suffisante pour distinguer deux blocages différents sans exposer de donnée perso
|
|
$empreinte = substr($ligne['ip_hash'], 0, 12);
|
|
$jusqua = date('d/m/Y à H:i', strtotime($ligne['locked_until']));
|
|
|
|
echo '<div class="project-card">';
|
|
echo '<div class="project-content">';
|
|
echo '<h2>IP bloquée</h2>';
|
|
echo '<p><strong>Empreinte :</strong> ' . htmlspecialchars($empreinte) . '…</p>';
|
|
echo '<p><strong>Tentatives ratées :</strong> ' . (int) $ligne['attempts'] . '</p>';
|
|
echo '<p><strong>Bloquée jusqu\'au :</strong> ' . htmlspecialchars($jusqua) . '</p>';
|
|
echo '<div class="admin-actions">';
|
|
echo '<a href="../scripts/unlockPresentationAttempt.php?ip=' . urlencode($ligne['ip_hash'])
|
|
. '&csrf_token=' . urlencode(csrfToken()) . '" class="btn-submit btn-delete"'
|
|
. ' onclick="return confirm(\'Débloquer cette IP ?\');">Débloquer</a>';
|
|
echo '</div>';
|
|
echo '</div>';
|
|
echo '</div>';
|
|
}
|
|
|
|
echo '</div>';
|
|
}
|