- Add .distignore (operator-private files excluded) - Add build.sh for WordPress-installable release ZIPs - Update CLAUDE.md references (now operator-private only)
337 lines
13 KiB
PHP
337 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* TigerStyle Scent Rate Limiter
|
|
* Implements secure rate limiting for OAuth2 endpoints to prevent abuse
|
|
*
|
|
* @package TigerStyle Scent
|
|
*/
|
|
|
|
defined('ABSPATH') or die('Direct access forbidden.');
|
|
|
|
class TigerStyleScent_RateLimiter {
|
|
|
|
/**
|
|
* Rate limiting configurations for different endpoints
|
|
* @var array
|
|
*/
|
|
private static $rate_limits = [
|
|
'oauth_authorize' => [
|
|
'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);
|
|
}
|
|
} |