ai/index.php
2026-02-01 10:02:12 +00:00

260 lines
9.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// index.php — Chatbot sécurisé LLaMA 2 7B via Ollama
session_start();
/* =======================
CONFIG & ENV
======================= */
$env = parse_ini_file(__DIR__ . '/config/.env');
define('SYSTEM_PROMPT', $env['SYSTEM_PROMPT'] ?? '');
define('CSRF_SECRET', $env['CSRF_SECRET'] ?? 'changez-moi');
define('RATE_LIMIT', 10); // 10 messages / minute
define('RATE_WINDOW', 60); // 60 sec
/* =======================
HEADERS SÉCURITÉ
======================= */
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('X-XSS-Protection: 1; mode=block');
/* =======================
SESSION INIT
======================= */
if (!isset($_SESSION['csrf'])) {
$_SESSION['csrf'] = hash_hmac('sha256', session_id(), CSRF_SECRET);
}
if (!isset($_SESSION['messages_timestamps'])) {
$_SESSION['messages_timestamps'] = [];
}
/* =======================
HELPERS
======================= */
function isAjax(): bool {
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
function sanitizeMessage(string $msg) {
$msg = trim($msg);
if ($msg === '' || mb_strlen($msg) > 500) return false;
return $msg;
}
/* =======================
VALIDATION CARACTERES
======================= */
function hasInvalidCharacters(string $msg): bool {
// Bloque TOUS les caractères spéciaux sauf ?, !, . - et '
// Accepte : lettres, chiffres, espaces, et {?, !, ., -, '}
return preg_match('/[^a-zA-Z0-9À-ÿ\s\?\!\.\-\']/u', $msg) === 1;
}
/* =======================
VERBES D'ACTION INTERDITS
======================= */
function hasBlockedVerbs(string $msg): bool {
// Bloque les verbes qui demandent une action spécifique à l'IA
$blockedVerbs = [
'montre', 'affiche', 'révèle', 'expose',
'donne', 'envoie', 'fournis', 'transmets',
'répète', 'résume', 'reformule', 'paraphrase', 'récite',
'dis-moi', 'dis moi', 'raconte', 'explique', 'CTF', 'CTFM1'
];
$msg_lower = mb_strtolower($msg);
foreach ($blockedVerbs as $verb) {
if (preg_match('/\b' . preg_quote($verb) . '\b/i', $msg) === 1) {
return true;
}
}
return false;
}
/* =======================
MOTS SENSIBLES INTERDITS
======================= */
function hasBlockedWords(string $msg): bool {
// Bloque les mots sensibles qui demandent des infos confidentielles
$blockedWords = [
'flag', 'ctf',
'token', 'secret', 'clé', 'key',
'motdepasse', 'mot de passe', 'password',
'prompt', 'instruction', 'système', 'system', 'règle', 'consigne'
];
$msg_lower = mb_strtolower($msg);
foreach ($blockedWords as $word) {
if (preg_match('/\b' . preg_quote($word) . '\b/i', $msg) === 1) {
return true;
}
}
return false;
}
/* =======================
RATE LIMITING
======================= */
function checkRateLimit(): bool {
$now = time();
$window_start = $now - RATE_WINDOW;
$_SESSION['messages_timestamps'] = array_values(
array_filter($_SESSION['messages_timestamps'], fn($ts) => $ts > $window_start)
);
if (count($_SESSION['messages_timestamps']) >= RATE_LIMIT) return false;
$_SESSION['messages_timestamps'][] = $now;
return true;
}
/* =======================
APPEL LLaMA via Ollama
======================= */
function callLlamaAPI(string $userMessage): string {
// Vérifications de sécurité avec messages explicites
if (hasInvalidCharacters($userMessage)) {
return "Je ne répondrai pas car certains caractères spéciaux sont interdits (sauf ?, !, ., - et '). Demande à Bouchra si tu veux savoir pourquoi.";
}
if (hasBlockedVerbs($userMessage)) {
return "Je ne répondrai pas car ce message contient un verbe d'action interdit (montre, donne, répète, etc). Demande à Bouchra si tu veux savoir pourquoi.";
}
if (hasBlockedWords($userMessage)) {
return "Je ne répondrai pas car ce message contient un mot sensible (flag, token, prompt, etc). Demande à Bouchra si tu veux savoir pourquoi.";
}
$payload = [
'model' => 'llama2:7b',
'messages' => [
['role'=>'system', 'content'=> SYSTEM_PROMPT],
['role'=>'user', 'content'=> $userMessage],
],
'max_tokens' => 800,
'temperature' => 0.7
];
$ch = curl_init('http://localhost:11434/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 300
]);
$response = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// DEBUG: Log l'erreur
error_log("Ollama API - HTTP: $http, Error: $error, Response: " . substr($response, 0, 200));
if ($error) {
return "Erreur de connexion à Ollama: $error";
}
if ($http !== 200) {
return "Erreur Ollama (HTTP $http). Vérifiez que le service tourne sur localhost:11434";
}
if (!$response) {
return "Pas de réponse du service LLaMA.";
}
$json = json_decode($response, true);
if (!$json || !isset($json['choices'][0]['message']['content'])) {
error_log("JSON decode error: " . json_last_error_msg());
return "Erreur: réponse LLaMA malformée.";
}
return $json['choices'][0]['message']['content'];
}
/* =======================
AJAX HANDLER
======================= */
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['message'])) {
if (!isAjax()) { http_response_code(403); echo json_encode(['error'=>'Requête interdite']); exit; }
if (!isset($_POST['csrf']) || $_POST['csrf'] !== $_SESSION['csrf']) { http_response_code(403); echo json_encode(['error'=>'CSRF invalide']); exit; }
if (!checkRateLimit()) { http_response_code(429); echo json_encode(['error'=>'LIMIT_REACHED']); exit; }
$message = sanitizeMessage($_POST['message']);
if ($message === false) { echo json_encode(['error'=>'Message invalide']); exit; }
$reply = callLlamaAPI($message);
echo json_encode(['response'=>$reply]);
exit;
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>bobentaz</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
body { margin:0; font-family:Inter,sans-serif; background:linear-gradient(135deg,#667eea,#764ba2); height:100vh; display:flex; justify-content:center; align-items:center; }
.chat-wrapper { width:100%; max-width:900px; height:700px; background:#fff; border-radius:24px; display:flex; flex-direction:column; overflow:hidden; }
.chat-header { padding:20px; background:linear-gradient(135deg,#667eea,#764ba2); color:#fff; font-weight:600; }
#chat { flex:1; padding:20px; background:#f8fafc; overflow-y:auto; }
.message { display:flex; margin-bottom:16px; }
.message.user { justify-content:flex-end; }
.message-bubble { max-width:70%; padding:12px 16px; border-radius:14px; font-size:14px; }
.user .message-bubble { background:#667eea; color:#fff; }
.bot .message-bubble { background:#fff; color:#1e293b; }
.input-area { padding:20px; border-top:1px solid #e5e7eb; }
#limitPopup { display:none; margin-bottom:10px; padding:10px; background:#fff3cd; color:#92400e; border-radius:10px; font-size:13px; text-align:center; }
form { display:flex; gap:10px; }
input { flex:1; padding:12px; border-radius:10px; border:1px solid #ccc; }
button { padding:12px 20px; border:none; border-radius:10px; background:#667eea; color:#fff; font-weight:600; cursor:pointer; }
button:disabled { opacity:.6; cursor:not-allowed; }
.rate-counter { font-size:12px; color:#666; margin-top:8px; text-align:center; }
</style>
</head>
<body>
<div class="chat-wrapper">
<div class="chat-header">Assistant IA (bobentaz)</div>
<div id="chat">
<div class="message bot">
<div class="message-bubble">Bonjour, comment puisje vous aider ?</div>
</div>
</div>
<div class="input-area">
<div id="limitPopup">Limite de 10 messages par minute atteinte. Veuillez réessayer plus tard.</div>
<form onsubmit="sendMsg();return false;">
<input type="hidden" id="csrf" value="<?= htmlspecialchars($_SESSION['csrf']) ?>">
<input id="userInput" type="text" placeholder="Votre message…" required>
<button id="sendBtn">Envoyer</button>
</form>
<div class="rate-counter">10 messages/minute</div>
</div>
</div>
<script>
const chat=document.getElementById('chat'),input=document.getElementById('userInput'),btn=document.getElementById('sendBtn'),csrf=document.getElementById('csrf').value,popup=document.getElementById('limitPopup');
function addMsg(text,isUser){const msg=document.createElement('div');msg.className='message '+(isUser?'user':'bot');const bubble=document.createElement('div');bubble.className='message-bubble';bubble.textContent=text;msg.appendChild(bubble);chat.appendChild(msg);chat.scrollTop=chat.scrollHeight;}
async function sendMsg(){const txt=input.value.trim();if(!txt)return;addMsg(txt,true);input.value='';btn.disabled=true;const res=await fetch('',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},body:'message='+encodeURIComponent(txt)+'&csrf='+encodeURIComponent(csrf)});const data=await res.json();if(data.error==='LIMIT_REACHED'){popup.style.display='block';addMsg('Limite atteinte. Attendez quelques secondes.',false);btn.disabled=false;return;}popup.style.display='none';addMsg(data.response||'Erreur.',false);btn.disabled=false;}
</script>
</body>
</html