| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  | <?php | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class RateLimiter { | 
					
						
							|  |  |  |     private $db; | 
					
						
							| 
									
										
										
										
											2025-01-03 15:02:49 +00:00
										 |  |  |     private $log; | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |     private $maxAttempts = 5;        // Maximum login attempts
 | 
					
						
							|  |  |  |     private $decayMinutes = 15;      // Time window in minutes
 | 
					
						
							| 
									
										
										
										
											2024-12-11 14:08:55 +00:00
										 |  |  |     private $ratelimitTable = 'login_attempts'; | 
					
						
							| 
									
										
										
										
											2024-12-12 14:11:41 +00:00
										 |  |  |     private $whitelistTable = 'ip_whitelist'; | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     public function __construct($database) { | 
					
						
							|  |  |  |         $this->db = $database->getConnection(); | 
					
						
							| 
									
										
										
										
											2025-01-03 15:02:49 +00:00
										 |  |  |         $this->log = new Log($database); | 
					
						
							| 
									
										
										
										
											2024-12-12 14:11:41 +00:00
										 |  |  |         $this->createTablesIfNotExists(); | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-11 14:00:13 +00:00
										 |  |  |     // Database preparation
 | 
					
						
							| 
									
										
										
										
											2024-12-12 14:11:41 +00:00
										 |  |  |     private function createTablesIfNotExists() { | 
					
						
							|  |  |  |         // Login attempts table
 | 
					
						
							| 
									
										
										
										
											2024-12-11 14:08:55 +00:00
										 |  |  |         $sql = "CREATE TABLE IF NOT EXISTS {$this->ratelimitTable} (
 | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |             id INTEGER PRIMARY KEY AUTOINCREMENT, | 
					
						
							| 
									
										
										
										
											2024-12-21 15:11:15 +00:00
										 |  |  |             ip_address TEXT NOT NULL, | 
					
						
							|  |  |  |             username TEXT NOT NULL, | 
					
						
							|  |  |  |             attempted_at TEXT DEFAULT (DATETIME('now')), | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |             INDEX idx_ip_username (ip_address, username) | 
					
						
							|  |  |  |         )";
 | 
					
						
							| 
									
										
										
										
											2024-12-12 14:11:41 +00:00
										 |  |  |         $this->db->exec($sql); | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-12 14:11:41 +00:00
										 |  |  |         // IP whitelist table
 | 
					
						
							|  |  |  |         $sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
 | 
					
						
							| 
									
										
										
										
											2024-12-20 15:05:45 +00:00
										 |  |  |             id INTEGER PRIMARY KEY AUTOINCREMENT, | 
					
						
							| 
									
										
										
										
											2024-12-21 15:11:15 +00:00
										 |  |  |             ip_address TEXT NOT NULL, | 
					
						
							|  |  |  |             is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)), | 
					
						
							|  |  |  |             description TEXT, | 
					
						
							|  |  |  |             created_at TEXT DEFAULT (DATETIME('now')), | 
					
						
							|  |  |  |             created_by TEXT, | 
					
						
							| 
									
										
										
										
											2024-12-12 14:11:41 +00:00
										 |  |  |             UNIQUE KEY unique_ip (ip_address) | 
					
						
							|  |  |  |         )";
 | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |         $this->db->exec($sql); | 
					
						
							| 
									
										
										
										
											2024-12-17 14:41:23 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // Default IPs to whitelist (local interface and private networks IPs)
 | 
					
						
							|  |  |  |         $defaultIps = [ | 
					
						
							|  |  |  |             ['127.0.0.1', false, 'localhost IPv4'], | 
					
						
							|  |  |  |             ['::1', false, 'localhost IPv6'], | 
					
						
							|  |  |  |             ['10.0.0.0/8', true, 'Private network (Class A)'], | 
					
						
							|  |  |  |             ['172.16.0.0/12', true, 'Private network (Class B)'], | 
					
						
							|  |  |  |             ['192.168.0.0/16', true, 'Private network (Class C)'] | 
					
						
							|  |  |  |         ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Insert default whitelisted IPs if they don't exist
 | 
					
						
							| 
									
										
										
										
											2024-12-21 15:18:53 +00:00
										 |  |  |         $stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->whitelistTable}
 | 
					
						
							|  |  |  |             (ip_address, is_network, description, created_by) | 
					
						
							| 
									
										
										
										
											2024-12-17 14:41:23 +00:00
										 |  |  |             VALUES (?, ?, ?, 'system')");
 | 
					
						
							|  |  |  |         foreach ($defaultIps as $ip) { | 
					
						
							|  |  |  |             $stmt->execute([$ip[0], $ip[1], $ip[2]]); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-11 14:00:13 +00:00
										 |  |  |     // Check if IP is whitelisted
 | 
					
						
							| 
									
										
										
										
											2024-12-09 13:43:10 +00:00
										 |  |  |     private function isIpWhitelisted($ip) { | 
					
						
							| 
									
										
										
										
											2024-12-12 14:16:48 +00:00
										 |  |  |         // Check exact IP match and CIDR ranges
 | 
					
						
							|  |  |  |          $stmt = $this->db->prepare("SELECT ip_address, is_network FROM {$this->whitelistTable}"); | 
					
						
							|  |  |  |          $stmt->execute(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |          while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { | 
					
						
							|  |  |  |              if ($row['is_network']) { | 
					
						
							|  |  |  |                  if ($this->ipInRange($ip, $row['ip_address'])) { | 
					
						
							|  |  |  |                      return true; | 
					
						
							|  |  |  |                  } | 
					
						
							|  |  |  |              } else { | 
					
						
							|  |  |  |                  if ($ip === $row['ip_address']) { | 
					
						
							|  |  |  |                      return true; | 
					
						
							|  |  |  |                  } | 
					
						
							|  |  |  |              } | 
					
						
							|  |  |  |          } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |          return false; | 
					
						
							| 
									
										
										
										
											2024-12-09 13:43:10 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private function ipInRange($ip, $cidr) { | 
					
						
							|  |  |  |         list($subnet, $bits) = explode('/', $cidr); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $ip = ip2long($ip); | 
					
						
							|  |  |  |         $subnet = ip2long($subnet); | 
					
						
							|  |  |  |         $mask = -1 << (32 - $bits); | 
					
						
							|  |  |  |         $subnet &= $mask; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return ($ip & $mask) == $subnet; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-11 14:00:13 +00:00
										 |  |  |     // Add to whitelist
 | 
					
						
							| 
									
										
										
										
											2024-12-21 15:14:31 +00:00
										 |  |  |     public function addToWhitelist($ip, $isNetwork = false, $description = '', $createdBy = 'system', $userId = null) { | 
					
						
							|  |  |  |         try { | 
					
						
							|  |  |  |             $stmt = $this->db->prepare("INSERT INTO {$this->whitelistTable}
 | 
					
						
							|  |  |  |                 (ip_address, is_network, description, created_by) | 
					
						
							|  |  |  |                 VALUES (?, ?, ?, ?) | 
					
						
							|  |  |  |                 ON DUPLICATE KEY UPDATE | 
					
						
							|  |  |  |                 is_network = VALUES(is_network), | 
					
						
							|  |  |  |                 description = VALUES(description), | 
					
						
							|  |  |  |                 created_by = VALUES(created_by)");
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 $result = $stmt->execute([$ip, $isNetwork, $description, $createdBy]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 if ($result) { | 
					
						
							|  |  |  |                     $logMessage = sprintf( | 
					
						
							|  |  |  |                         'IP Whitelist: Added %s "%s" by %s. Description: %s', | 
					
						
							|  |  |  |                         $isNetwork ? 'network' : 'IP', | 
					
						
							|  |  |  |                         $ip, | 
					
						
							|  |  |  |                         $createdBy, | 
					
						
							|  |  |  |                         $description | 
					
						
							|  |  |  |                     ); | 
					
						
							|  |  |  |                     $this->log->insertLog($userId ?? 0, $logMessage, 'system'); | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             return $result; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         } catch (Exception $e) { | 
					
						
							|  |  |  |             if ($userId) { | 
					
						
							|  |  |  |                 $this->log->insertLog($userId, "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), 'system'); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-12-09 13:44:00 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-11 14:00:13 +00:00
										 |  |  |     // Remove from whitelist
 | 
					
						
							| 
									
										
										
										
											2024-12-09 13:44:00 +00:00
										 |  |  |     public function removeFromWhitelist($ip) { | 
					
						
							| 
									
										
										
										
											2024-12-12 14:16:48 +00:00
										 |  |  |         $stmt = $this->db->prepare("DELETE FROM {$this->whitelistTable} WHERE ip_address = ?"); | 
					
						
							| 
									
										
										
										
											2024-12-09 13:44:00 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-12 14:16:48 +00:00
										 |  |  |         return $stmt->execute([$ip]); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function getWhitelistedIps() { | 
					
						
							|  |  |  |         $stmt = $this->db->prepare("SELECT * FROM {$this->whitelistTable} ORDER BY created_at DESC"); | 
					
						
							|  |  |  |         $stmt->execute(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $stmt->fetchAll(PDO::FETCH_ASSOC); | 
					
						
							| 
									
										
										
										
											2024-12-09 13:44:00 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |     public function attempt($username, $ipAddress) { | 
					
						
							| 
									
										
										
										
											2024-12-10 13:56:18 +00:00
										 |  |  |         // Skip rate limiting for whitelisted IPs
 | 
					
						
							|  |  |  |         if ($this->isIpWhitelisted($ipAddress)) { | 
					
						
							|  |  |  |             return true; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |         // Clean old attempts
 | 
					
						
							|  |  |  |         $this->clearOldAttempts(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Record this attempt
 | 
					
						
							| 
									
										
										
										
											2024-12-11 14:08:55 +00:00
										 |  |  |         $sql = "INSERT INTO {$this->ratelimitTable} (ip_address, username) VALUES (:ip, :username)"; | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |         $stmt = $this->db->prepare($sql); | 
					
						
							|  |  |  |         $stmt->execute([ | 
					
						
							|  |  |  |             ':ip'		=> $ipAddress, | 
					
						
							|  |  |  |             ':username'		=> $username | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Check if too many attempts
 | 
					
						
							|  |  |  |         return !$this->tooManyAttempts($username, $ipAddress); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function tooManyAttempts($username, $ipAddress) { | 
					
						
							| 
									
										
										
										
											2024-12-10 13:56:58 +00:00
										 |  |  |         $sql = "SELECT COUNT(*) as attempts
 | 
					
						
							| 
									
										
										
										
											2024-12-11 14:08:55 +00:00
										 |  |  |                 FROM {$this->ratelimitTable} | 
					
						
							| 
									
										
										
										
											2024-12-10 13:56:58 +00:00
										 |  |  |                 WHERE ip_address = :ip | 
					
						
							|  |  |  |                 AND username = :username | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |                 AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $stmt = $this->db->prepare($sql); | 
					
						
							|  |  |  |         $stmt->execute([ | 
					
						
							|  |  |  |             ':ip'		=> $ipAddress, | 
					
						
							|  |  |  |             ':username'		=> $username, | 
					
						
							|  |  |  |             ':minutes'		=> $this->decayMinutes | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $result = $stmt->fetch(PDO::FETCH_ASSOC); | 
					
						
							|  |  |  |         return $result['attempts'] >= $this->maxAttempts; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function clearOldAttempts() { | 
					
						
							| 
									
										
										
										
											2024-12-11 14:08:55 +00:00
										 |  |  |         $sql = "DELETE FROM {$this->ratelimitTable}
 | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |                 WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')";
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $stmt = $this->db->prepare($sql); | 
					
						
							|  |  |  |         $stmt->execute([ | 
					
						
							|  |  |  |             ':minutes'		=> $this->decayMinutes | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function getRemainingAttempts($username, $ipAddress) { | 
					
						
							| 
									
										
										
										
											2024-12-10 13:56:58 +00:00
										 |  |  |         $sql = "SELECT COUNT(*) as attempts
 | 
					
						
							| 
									
										
										
										
											2024-12-11 14:08:55 +00:00
										 |  |  |                 FROM {$this->ratelimitTable} | 
					
						
							| 
									
										
										
										
											2024-12-10 13:56:58 +00:00
										 |  |  |                 WHERE ip_address = :ip | 
					
						
							|  |  |  |                 AND username = :username | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |                 AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $stmt = $this->db->prepare($sql); | 
					
						
							|  |  |  |         $stmt->execute([ | 
					
						
							|  |  |  |             ':ip'		=> $ipAddress, | 
					
						
							|  |  |  |             ':username'		=> $username, | 
					
						
							|  |  |  |             ':minutes'		=> $this->decayMinutes | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $result = $stmt->fetch(PDO::FETCH_ASSOC); | 
					
						
							|  |  |  |         return max(0, $this->maxAttempts - $result['attempts']); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function getDecayMinutes() { | 
					
						
							|  |  |  |         return $this->decayMinutes; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |