[ 'limit' => 30, // 30 requests 'window' => 3600, // per hour 'block_duration' => 3600 // 1 hour block ], 'oauth_token' => [ 'limit' => 60, // 60 requests 'window' => 3600, // per hour 'block_duration' => 1800 // 30 min block ], 'oauth_introspect' => [ 'limit' => 100, // 100 requests 'window' => 3600, // per hour 'block_duration' => 900 // 15 min block ], 'oauth_revoke' => [ 'limit' => 20, // 20 requests 'window' => 3600, // per hour 'block_duration' => 1800 // 30 min block ] ]; /** * Check if request is within rate limits * * @param string $endpoint OAuth2 endpoint (authorize, token, introspect, revoke) * @param string $identifier IP address or client identifier * @return bool True if within limits, false if rate limited */ public static function check_rate_limit(string $endpoint, string $identifier = null): bool { // Get client identifier (IP address or authenticated client) if (!$identifier) { $identifier = self::get_client_identifier(); } $endpoint_key = 'oauth_' . $endpoint; // Check if endpoint has rate limiting configured if (!isset(self::$rate_limits[$endpoint_key])) { return true; // No rate limiting configured } $config = self::$rate_limits[$endpoint_key]; // Check if client is currently blocked if (self::is_blocked($endpoint_key, $identifier)) { self::log_rate_limit_violation($endpoint_key, $identifier, 'blocked'); return false; } // Get current request count $current_count = self::get_request_count($endpoint_key, $identifier); // 🔐 SECURITY: Check if limit exceeded with progressive delays if ($current_count >= $config['limit']) { // Calculate progressive block duration based on violations $violation_count = self::get_violation_count($endpoint_key, $identifier); $progressive_duration = self::calculate_progressive_duration($config['block_duration'], $violation_count); // Block the client with progressive duration self::block_client($endpoint_key, $identifier, $progressive_duration); self::increment_violation_count($endpoint_key, $identifier); self::log_rate_limit_violation($endpoint_key, $identifier, 'limit_exceeded'); return false; } // Add progressive delay for clients approaching limit if ($current_count > ($config['limit'] * 0.8)) { $delay = min(2, ($current_count / $config['limit']) * 2); // Max 2 second delay if ($delay > 0.1) { usleep($delay * 1000000); // Convert to microseconds } } // Increment request count self::increment_request_count($endpoint_key, $identifier, $config['window']); return true; } /** * Get unique client identifier for rate limiting * * @return string Client identifier */ private static function get_client_identifier(): string { // 🔐 SECURITY: Enhanced client fingerprinting to prevent spoofing $ip = self::get_client_ip(); // Create more robust fingerprint using multiple headers $fingerprint_data = [ 'ip' => $ip, 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'accept_language' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '', 'accept_encoding' => $_SERVER['HTTP_ACCEPT_ENCODING'] ?? '', 'connection' => $_SERVER['HTTP_CONNECTION'] ?? '', ]; // Use HMAC with a secret key for tamper resistance $secret_key = defined('TIGERSTYLE_SCENT_SECRET_KEY') ? TIGERSTYLE_SCENT_SECRET_KEY : wp_salt('auth'); $fingerprint = hash_hmac('sha256', json_encode($fingerprint_data), $secret_key); return $ip . '_' . substr($fingerprint, 0, 16); } /** * Get real client IP address (accounting for proxies) * * @return string IP address */ private static function get_client_ip(): string { // Check for IP from various headers (in order of preference) $ip_headers = [ 'HTTP_CF_CONNECTING_IP', // Cloudflare 'HTTP_X_REAL_IP', // Nginx proxy 'HTTP_X_FORWARDED_FOR', // Standard proxy header 'HTTP_X_FORWARDED', // Alternative 'HTTP_X_CLUSTER_CLIENT_IP', // Cluster 'HTTP_FORWARDED_FOR', // Alternative 'HTTP_FORWARDED', // RFC 7239 'REMOTE_ADDR' // Standard ]; foreach ($ip_headers as $header) { if (!empty($_SERVER[$header])) { $ip = $_SERVER[$header]; // Handle comma-separated IPs (take first one) if (strpos($ip, ',') !== false) { $ip = trim(explode(',', $ip)[0]); } // Validate IP address if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $ip; } } } // Fallback to REMOTE_ADDR (even if private) return $_SERVER['REMOTE_ADDR'] ?? 'unknown'; } /** * Check if client is currently blocked * * @param string $endpoint_key Endpoint identifier * @param string $identifier Client identifier * @return bool True if blocked */ private static function is_blocked(string $endpoint_key, string $identifier): bool { $block_key = "tigerstyle_scent_block_{$endpoint_key}_{$identifier}"; return (bool) get_transient($block_key); } /** * Block client for specified duration * * @param string $endpoint_key Endpoint identifier * @param string $identifier Client identifier * @param int $duration Block duration in seconds */ private static function block_client(string $endpoint_key, string $identifier, int $duration): void { $block_key = "tigerstyle_scent_block_{$endpoint_key}_{$identifier}"; set_transient($block_key, time(), $duration); // Also clear request count when blocking $count_key = "tigerstyle_scent_count_{$endpoint_key}_{$identifier}"; delete_transient($count_key); } /** * Get current request count for client * * @param string $endpoint_key Endpoint identifier * @param string $identifier Client identifier * @return int Current request count */ private static function get_request_count(string $endpoint_key, string $identifier): int { $count_key = "tigerstyle_scent_count_{$endpoint_key}_{$identifier}"; return (int) get_transient($count_key); } /** * Increment request count for client * * @param string $endpoint_key Endpoint identifier * @param string $identifier Client identifier * @param int $window Time window in seconds */ private static function increment_request_count(string $endpoint_key, string $identifier, int $window): void { $count_key = "tigerstyle_scent_count_{$endpoint_key}_{$identifier}"; $current_count = self::get_request_count($endpoint_key, $identifier); set_transient($count_key, $current_count + 1, $window); } /** * Log rate limit violation for monitoring * * @param string $endpoint_key Endpoint identifier * @param string $identifier Client identifier * @param string $violation_type Type of violation */ private static function log_rate_limit_violation(string $endpoint_key, string $identifier, string $violation_type): void { $log_data = [ 'endpoint' => $endpoint_key, 'client' => $identifier, 'violation' => $violation_type, 'timestamp' => current_time('mysql'), 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown' ]; // Log to WordPress error log if debug enabled if (defined('TIGERSTYLE_SCENT_DEBUG') && TIGERSTYLE_SCENT_DEBUG) { error_log('[TigerStyle Scent Rate Limit] ' . json_encode($log_data)); } // Fire action for external monitoring systems do_action('tigerstyle_scent_rate_limit_violation', $log_data); } /** * Send rate limit exceeded response * * @param string $endpoint Endpoint name */ public static function send_rate_limit_response(string $endpoint): void { $config = self::$rate_limits['oauth_' . $endpoint] ?? []; $retry_after = $config['block_duration'] ?? 3600; http_response_code(429); header('Content-Type: application/json'); header('Retry-After: ' . $retry_after); header('X-RateLimit-Limit: ' . ($config['limit'] ?? 'N/A')); header('X-RateLimit-Window: ' . ($config['window'] ?? 'N/A')); echo json_encode([ 'error' => 'rate_limit_exceeded', 'error_description' => 'Too many requests. Territory access temporarily restricted.', 'retry_after' => $retry_after ]); exit; } /** * Get rate limit status for client * * @param string $endpoint Endpoint name * @param string $identifier Client identifier * @return array Rate limit status */ public static function get_rate_limit_status(string $endpoint, string $identifier = null): array { if (!$identifier) { $identifier = self::get_client_identifier(); } $endpoint_key = 'oauth_' . $endpoint; $config = self::$rate_limits[$endpoint_key] ?? []; if (empty($config)) { return ['rate_limited' => false]; } $current_count = self::get_request_count($endpoint_key, $identifier); $is_blocked = self::is_blocked($endpoint_key, $identifier); return [ 'rate_limited' => $is_blocked || ($current_count >= $config['limit']), 'current_count' => $current_count, 'limit' => $config['limit'], 'window' => $config['window'], 'remaining' => max(0, $config['limit'] - $current_count), 'blocked' => $is_blocked ]; } /** * Get violation count for progressive penalties * * @param string $endpoint_key Endpoint identifier * @param string $identifier Client identifier * @return int Violation count */ private static function get_violation_count(string $endpoint_key, string $identifier): int { $violation_key = "tigerstyle_scent_violations_{$endpoint_key}_{$identifier}"; return (int) get_transient($violation_key); } /** * Increment violation count for progressive penalties * * @param string $endpoint_key Endpoint identifier * @param string $identifier Client identifier */ private static function increment_violation_count(string $endpoint_key, string $identifier): void { $violation_key = "tigerstyle_scent_violations_{$endpoint_key}_{$identifier}"; $current_violations = self::get_violation_count($endpoint_key, $identifier); // Violations expire after 24 hours set_transient($violation_key, $current_violations + 1, DAY_IN_SECONDS); } /** * Calculate progressive block duration based on violation history * * @param int $base_duration Base block duration in seconds * @param int $violation_count Number of previous violations * @return int Progressive duration in seconds */ private static function calculate_progressive_duration(int $base_duration, int $violation_count): int { // Progressive multiplier: 1x, 2x, 4x, 8x, max 24 hours $multiplier = min(pow(2, $violation_count), 24); $progressive_duration = $base_duration * $multiplier; // Cap at 24 hours maximum return min($progressive_duration, DAY_IN_SECONDS); } }