<?php
/**
 * security.php - Système de sécurité centralisé
 * 
 * Fonctionnalités :
 * 1. TOKEN AUTH : Génération et validation de tokens JWT-like (HMAC-SHA256 + expiration)
 * 2. RATE LIMITING : Limitation du nombre de requêtes par IP
 * 3. IP WHITELIST : Liste blanche d'adresses IP autorisées
 * 4. REQUEST SIGNING : Vérification optionnelle de signature HMAC
 * 
 * Utilisation :
 *   require_once 'security.php';
 *   $auth = new ApiSecurity();
 *   $auth->requireAuth(); // Valide le token, applique rate limiting, whitelist
 */

// Fichier de configuration (à placer HORS du web root si possible)
define('SECURITY_CONFIG_FILE', __DIR__ . '/security_config.json');
define('RATE_LIMIT_FILE', __DIR__ . '/rate_limit_cache.json');
define('TOKEN_SECRET_FILE', __DIR__ . '/token_secret.key');

class ApiSecurity {
    
    private $config;
    private $secretKey;
    
    public function __construct() {
        $this->loadConfig();
        $this->loadSecretKey();
    }
    
    // ==========================================
    // CHARGEMENT DE LA CONFIGURATION
    // ==========================================
    
    private function loadConfig() {
        $defaultConfig = [
            'token_expiry' => 86400,        // 24 heures en secondes
            'token_renew_threshold' => 3600, // Renouveler si moins de 1h restante
            'rate_limit_enabled' => true,
            'rate_limit_max_requests' => 120, // Nombre max de requêtes
            'rate_limit_window' => 60,       // Fenêtre en secondes (1 minute)
            'rate_limit_ban_duration' => 300, // Bannissement temporaire (5 min)
            'rate_limit_ban_threshold' => 3,  // Nombre de dépassements avant bannissement
            'whitelist_enabled' => false,
            'whitelisted_ips' => [],
            'signature_enabled' => false,
            'signature_timeout' => 300,      // 5 minutes de tolérance pour l'horodatage
        ];
        
        if (file_exists(SECURITY_CONFIG_FILE)) {
            $fileConfig = json_decode(file_get_contents(SECURITY_CONFIG_FILE), true);
            if (is_array($fileConfig)) {
                $this->config = array_merge($defaultConfig, $fileConfig);
                return;
            }
        }
        
        $this->config = $defaultConfig;
    }
    
    private function loadSecretKey() {
        // Générer ou charger une clé secrète persistante
        if (file_exists(TOKEN_SECRET_FILE)) {
            $this->secretKey = trim(file_get_contents(TOKEN_SECRET_FILE));
        } else {
            // Générer une clé aléatoire de 256 bits (32 octets)
            $this->secretKey = bin2hex(random_bytes(32));
            file_put_contents(TOKEN_SECRET_FILE, $this->secretKey, LOCK_EX);
            chmod(TOKEN_SECRET_FILE, 0600); // Lecture/écriture uniquement pour le propriétaire
        }
    }
    
    // ==========================================
    // GESTION DES TOKENS (HMAC-SHA256)
    // ==========================================
    
    /**
     * Génère un token pour un utilisateur
     * Format : base64(header).base64(payload).base64(signature)
     * 
     * @param string $userId Identifiant de l'utilisateur
     * @param string $role Rôle de l'utilisateur
     * @param string $siteID Site de l'utilisateur
     * @return string Token généré
     */
    public function generateToken($userId, $role, $siteID) {
        $header = [
            'alg' => 'HS256',
            'typ' => 'JWT'
        ];
        
        $now = time();
        $payload = [
            'sub' => $userId,           // Subject (utilisateur)
            'role' => $role,            // Rôle
            'site' => $siteID,          // Site
            'iat' => $now,              // Issued at
            'exp' => $now + $this->config['token_expiry'], // Expiration
            'jti' => bin2hex(random_bytes(16)) // ID unique du token (pour révocation)
        ];
        
        $headerB64 = $this->base64UrlEncode(json_encode($header));
        $payloadB64 = $this->base64UrlEncode(json_encode($payload));
        
        $signature = $this->signToken($headerB64, $payloadB64);
        $signatureB64 = $this->base64UrlEncode($signature);
        
        return $headerB64 . '.' . $payloadB64 . '.' . $signatureB64;
    }
    
    /**
     * Vérifie et décode un token
     * 
     * @param string $token Le token à vérifier
     * @return array|false Les données du token ou false si invalide
     */
    public function validateToken($token) {
        if (empty($token)) {
            return false;
        }
        
        $parts = explode('.', $token);
        if (count($parts) !== 3) {
            return false;
        }
        
        list($headerB64, $payloadB64, $signatureB64) = $parts;
        
        // Vérifier la signature
        $expectedSignature = $this->signToken($headerB64, $payloadB64);
        $providedSignature = $this->base64UrlDecode($signatureB64);
        
        if (!hash_equals($expectedSignature, $providedSignature)) {
            return false; // Signature invalide
        }
        
        // Décoder le payload
        $payload = json_decode($this->base64UrlDecode($payloadB64), true);
        if (!$payload || !isset($payload['exp'])) {
            return false;
        }
        
        // Vérifier l'expiration
        if ($payload['exp'] < time()) {
            return false; // Token expiré
        }
        
        return $payload;
    }
    
    /**
     * Calcule la signature HMAC-SHA256
     */
    private function signToken($headerB64, $payloadB64) {
        // Utilisation d'une clé dérivée pour éviter les attaques de extension de longueur
        $key = hash_hmac('sha256', 'token-signature-v1', $this->secretKey, true);
        return hash_hmac('sha256', $headerB64 . '.' . $payloadB64, $key, true);
    }
    
    /**
     * Vérifie si un token doit être renouvelé (proche de l'expiration)
     */
    public function shouldRenewToken($token) {
        $payload = $this->validateToken($token);
        if (!$payload) {
            return false;
        }
        
        $remaining = $payload['exp'] - time();
        return $remaining < $this->config['token_renew_threshold'];
    }
    
    /**
     * Calcule une signature HMAC pour une requête (anti-replay)
     * 
     * @param string $method Méthode HTTP (GET, POST)
     * @param string $uri URI de la requête
     * @param string $body Corps de la requête
     * @param int $timestamp Horodatage Unix
     * @param string $token Token d'authentification
     * @return string Signature calculée
     */
    public function signRequest($method, $uri, $body, $timestamp, $token) {
        $data = implode("\n", [
            strtoupper($method),
            $uri,
            hash('sha256', $body),
            (string)$timestamp,
            $token
        ]);
        $key = hash_hmac('sha256', 'request-signing-v1', $this->secretKey, true);
        return $this->base64UrlEncode(hash_hmac('sha256', $data, $key, true));
    }
    
    /**
     * Vérifie la signature d'une requête
     */
    public function verifyRequestSignature($method, $uri, $body, $timestamp, $token, $signature) {
        if (empty($signature)) {
            return false;
        }
        
        // Vérifier que l'horodatage n'est pas trop ancien
        $now = time();
        if (abs($now - (int)$timestamp) > $this->config['signature_timeout']) {
            return false;
        }
        
        $expected = $this->signRequest($method, $uri, $body, $timestamp, $token);
        return hash_equals($expected, $signature);
    }
    
    // ==========================================
    // RATE LIMITING (par IP)
    // ==========================================
    
    /**
     * Vérifie et incrémente le compteur de rate limiting
     * 
     * @return array ['allowed' => true/false, 'remaining' => nb_requetes_restantes, 'retry_after' => secondes]
     */
    public function checkRateLimit() {
        if (!$this->config['rate_limit_enabled']) {
            return ['allowed' => true, 'remaining' => -1, 'retry_after' => 0];
        }
        
        $ip = $this->getClientIP();
        $now = time();
        $limits = $this->loadRateLimits();
        
        // Initialiser si nouvelle IP
        if (!isset($limits[$ip])) {
            $limits[$ip] = [
                'count' => 0,
                'window_start' => $now,
                'ban_count' => 0,
                'ban_until' => 0
            ];
        }
        
        $entry = &$limits[$ip];
        
        // Vérifier si banni
        if ($entry['ban_until'] > $now) {
            $retryAfter = $entry['ban_until'] - $now;
            return ['allowed' => false, 'remaining' => 0, 'retry_after' => $retryAfter, 'reason' => 'banned'];
        }
        
        // Réinitialiser la fenêtre si expirée
        if ($entry['window_start'] + $this->config['rate_limit_window'] < $now) {
            $entry['count'] = 0;
            $entry['window_start'] = $now;
        }
        
        // Vérifier le seuil
        $entry['count']++;
        $remaining = max(0, $this->config['rate_limit_max_requests'] - $entry['count']);
        
        if ($entry['count'] > $this->config['rate_limit_max_requests']) {
            // Incrémenter le compteur de bannissement
            $entry['ban_count']++;
            
            // Bannir si dépassement multiple
            if ($entry['ban_count'] >= $this->config['rate_limit_ban_threshold']) {
                $entry['ban_until'] = $now + $this->config['rate_limit_ban_duration'];
                $entry['count'] = 0;
                $this->saveRateLimits($limits);
                
                $this->logSecurity('rate_limit_ban', [
                    'ip' => $ip,
                    'ban_duration' => $this->config['rate_limit_ban_duration']
                ]);
                
                return ['allowed' => false, 'remaining' => 0, 'retry_after' => $this->config['rate_limit_ban_duration'], 'reason' => 'banned'];
            }
            
            $this->saveRateLimits($limits);
            return ['allowed' => false, 'remaining' => 0, 'retry_after' => $this->config['rate_limit_window'], 'reason' => 'rate_exceeded'];
        }
        
        // Nettoyer les entrées anciennes (une fois sur 10)
        if (rand(1, 10) === 1) {
            $this->cleanOldEntries($limits, $now);
        }
        
        $this->saveRateLimits($limits);
        return ['allowed' => true, 'remaining' => $remaining, 'retry_after' => 0];
    }
    
    private function loadRateLimits() {
        if (file_exists(RATE_LIMIT_FILE)) {
            $data = json_decode(file_get_contents(RATE_LIMIT_FILE), true);
            if (is_array($data)) return $data;
        }
        return [];
    }
    
    private function saveRateLimits($limits) {
        // Utiliser un fichier temporaire pour éviter la corruption
        $tmp = RATE_LIMIT_FILE . '.tmp';
        file_put_contents($tmp, json_encode($limits), LOCK_EX);
        rename($tmp, RATE_LIMIT_FILE);
    }
    
    private function cleanOldEntries(&$limits, $now) {
        $threshold = $now - ($this->config['rate_limit_window'] * 10);
        $banThreshold = $now - $this->config['rate_limit_ban_duration'];
        
        foreach ($limits as $ip => $entry) {
            if ($entry['window_start'] < $threshold && $entry['ban_until'] < $banThreshold) {
                unset($limits[$ip]);
            }
        }
    }
    
    // ==========================================
    // WHITELIST IP
    // ==========================================
    
    /**
     * Vérifie si l'IP du client est autorisée
     */
    public function checkWhitelist() {
        if (!$this->config['whitelist_enabled']) {
            return true;
        }
        
        $ip = $this->getClientIP();
        
        // Vérifier si l'IP est dans la liste blanche
        if (in_array($ip, $this->config['whitelisted_ips'])) {
            return true;
        }
        
        // Vérifier les CIDR (ex: 192.168.1.0/24)
        foreach ($this->config['whitelisted_ips'] as $allowed) {
            if (strpos($allowed, '/') !== false) {
                if ($this->ipInCIDR($ip, $allowed)) {
                    return true;
                }
            }
        }
        
        $this->logSecurity('whitelist_blocked', ['ip' => $ip]);
        return false;
    }
    
    /**
     * Vérifie si une IP est dans un range CIDR
     */
    private function ipInCIDR($ip, $cidr) {
        list($subnet, $bits) = explode('/', $cidr);
        $ip = ip2long($ip);
        $subnet = ip2long($subnet);
        $mask = -1 << (32 - $bits);
        return ($ip & $mask) === ($subnet & $mask);
    }
    
    // ==========================================
    // UTILITAIRES
    // ==========================================
    
    /**
     * Récupère l'IP réelle du client (derrière proxy)
     */
    private function getClientIP() {
        // Vérifier les headers de proxy
        $headers = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];
        
        foreach ($headers as $h) {
            if (isset($_SERVER[$h])) {
                $ip = $_SERVER[$h];
                // Si plusieurs IP (forwarded), prendre la première
                if (strpos($ip, ',') !== false) {
                    $ip = trim(explode(',', $ip)[0]);
                }
                if (filter_var($ip, FILTER_VALIDATE_IP)) {
                    return $ip;
                }
            }
        }
        
        return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    }
    
    /**
     * Encode en base64 URL-safe (sans padding)
     */
    private function base64UrlEncode($data) {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }
    
    /**
     * Décode du base64 URL-safe
     */
    private function base64UrlDecode($data) {
        $remainder = strlen($data) % 4;
        if ($remainder) {
            $data .= str_repeat('=', 4 - $remainder);
        }
        return base64_decode(strtr($data, '-_', '+/'));
    }
    
    /**
     * Journalisation des événements de sécurité
     */
    public function logSecurity($event, $data = []) {
        $logFile = __DIR__ . '/security_events.log';
        $logEntry = [
            'timestamp' => date('Y-m-d H:i:s'),
            'event' => $event,
            'ip' => $this->getClientIP(),
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
            'uri' => $_SERVER['REQUEST_URI'] ?? 'unknown',
            'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
            'data' => $data
        ];
        
        $line = json_encode($logEntry) . "\n";
        file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX);
        
        // Limiter la taille du fichier de log (max 10MB)
        if (filesize($logFile) > 10 * 1024 * 1024) {
            $this->rotateLog($logFile);
        }
    }
    
    private function rotateLog($file) {
        $backup = $file . '.1';
        if (file_exists($backup)) {
            unlink($backup);
        }
        rename($file, $backup);
    }
    
    // ==========================================
    // POINT D'ENTRÉE PRINCIPAL
    // ==========================================
    
    /**
     * Point d'entrée principal : valide toutes les sécurités
     * 
     * @param bool $requireToken Si true, le token est obligatoire
     * @return array|false Les données du token si valide, sinon false + envoie une réponse HTTP 401/429/403
     */
    public function requireAuth($requireToken = true) {
        // 1. Vérifier la whitelist IP
        if (!$this->checkWhitelist()) {
            $this->sendSecurityError(403, 'IP non autorisée');
            exit;
        }
        
        // 2. Vérifier le rate limiting
        $rateCheck = $this->checkRateLimit();
        if (!$rateCheck['allowed']) {
            header('Retry-After: ' . $rateCheck['retry_after']);
            $this->sendSecurityError(429, 'Trop de requêtes. Réessayez dans ' . $rateCheck['retry_after'] . ' secondes.');
            exit;
        }
        
        // 3. Si pas besoin de token, on arrête là
        if (!$requireToken) {
            return true;
        }
        
        // 4. Récupérer le token (header Authorization ou paramètre token)
        $token = $this->getBearerToken();
        if (empty($token)) {
            $this->sendSecurityError(401, 'Token d\'authentification requis');
            exit;
        }
        
        // 5. Valider le token
        $payload = $this->validateToken($token);
        if (!$payload) {
            $this->logSecurity('invalid_token', ['token_prefix' => substr($token, 0, 20) . '...']);
            $this->sendSecurityError(401, 'Token invalide ou expiré');
            exit;
        }
        
        // 6. Vérifier la signature de la requête si activée
        if ($this->config['signature_enabled']) {
            $signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
            $timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
            $body = file_get_contents('php://input');
            $uri = $_SERVER['REQUEST_URI'];
            
            if (!$this->verifyRequestSignature($_SERVER['REQUEST_METHOD'], $uri, $body, $timestamp, $token, $signature)) {
                $this->logSecurity('invalid_signature', ['ip' => $this->getClientIP()]);
                $this->sendSecurityError(401, 'Signature de requête invalide');
                exit;
            }
        }
        
        // 7. Retourner les données du token
        return $payload;
    }
    
    /**
     * Récupère le token depuis le header Authorization ou les paramètres
     */
    private function getBearerToken() {
        // Header Authorization: Bearer <token>
        $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
        if (preg_match('/Bearer\s+(.+)$/i', $authHeader, $matches)) {
            return $matches[1];
        }
        
        // Paramètre GET/POST
        return $_REQUEST['token'] ?? '';
    }
    
    /**
     * Envoie une erreur de sécurité formatée
     */
    private function sendSecurityError($code, $message) {
        http_response_code($code);
        header('Content-Type: application/json; charset=utf-8');
        header('X-Security-Status: ' . $code);
        
        $response = [
            'success' => false,
            'message' => $message,
            'code' => $code
        ];
        
        echo json_encode($response, JSON_UNESCAPED_UNICODE);
    }
}

/**
 * Fonction helper pour usage rapide
 */
function requireAuth() {
    static $security = null;
    if ($security === null) {
        $security = new ApiSecurity();
    }
    return $security->requireAuth();
}
?>