| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  | <?php | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class RateLimiter { | 
					
						
							| 
									
										
										
										
											2025-01-04 10:30:44 +00:00
										 |  |  |     public $db; | 
					
						
							| 
									
										
										
										
											2025-01-03 15:02:49 +00:00
										 |  |  |     private $log; | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |     public $maxAttempts = 5;           // Maximum login attempts
 | 
					
						
							|  |  |  |     public $decayMinutes = 15;         // Time window in minutes
 | 
					
						
							| 
									
										
										
										
											2025-01-04 10:30:44 +00:00
										 |  |  |     public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
 | 
					
						
							|  |  |  |     public $autoBlacklistDuration = 24;  // Hours to blacklist for
 | 
					
						
							|  |  |  |     public $ratelimitTable = 'login_attempts'; | 
					
						
							|  |  |  |     public $whitelistTable = 'ip_whitelist'; | 
					
						
							|  |  |  |     public $blacklistTable = 'ip_blacklist'; | 
					
						
							| 
									
										
										
										
											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-29 15:35:15 +00:00
										 |  |  |         $this->createTablesIfNotExist(); | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-11 14:00:13 +00:00
										 |  |  |     // Database preparation
 | 
					
						
							| 
									
										
										
										
											2024-12-29 15:35:15 +00:00
										 |  |  |     private function createTablesIfNotExist() { | 
					
						
							| 
									
										
										
										
											2024-12-12 14:11:41 +00:00
										 |  |  |         // 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, | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |             ip_address TEXT NOT NULL UNIQUE, | 
					
						
							| 
									
										
										
										
											2024-12-21 15:11:15 +00:00
										 |  |  |             username TEXT NOT NULL, | 
					
						
							| 
									
										
										
										
											2025-01-03 16:44:08 +00:00
										 |  |  |             attempted_at TEXT DEFAULT (DATETIME('now')) | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |         )";
 | 
					
						
							| 
									
										
										
										
											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, | 
					
						
							| 
									
										
										
										
											2025-01-02 15:46:28 +00:00
										 |  |  |             ip_address TEXT NOT NULL UNIQUE, | 
					
						
							| 
									
										
										
										
											2024-12-21 15:11:15 +00:00
										 |  |  |             is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)), | 
					
						
							|  |  |  |             description TEXT, | 
					
						
							|  |  |  |             created_at TEXT DEFAULT (DATETIME('now')), | 
					
						
							| 
									
										
										
										
											2025-01-02 15:46:28 +00:00
										 |  |  |             created_by TEXT | 
					
						
							|  |  |  |         )";
 | 
					
						
							|  |  |  |         $this->db->exec($sql); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // IP blacklist table
 | 
					
						
							|  |  |  |         $sql = "CREATE TABLE IF NOT EXISTS {$this->blacklistTable} (
 | 
					
						
							|  |  |  |             id INTEGER PRIMARY KEY AUTOINCREMENT, | 
					
						
							|  |  |  |             ip_address TEXT NOT NULL UNIQUE, | 
					
						
							|  |  |  |             is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)), | 
					
						
							|  |  |  |             reason TEXT, | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |             expiry_time TEXT  NULL, | 
					
						
							| 
									
										
										
										
											2025-01-02 15:46:28 +00:00
										 |  |  |             created_at TEXT DEFAULT (DATETIME('now')), | 
					
						
							|  |  |  |             created_by TEXT | 
					
						
							| 
									
										
										
										
											2024-12-12 14:11:41 +00:00
										 |  |  |         )";
 | 
					
						
							| 
									
										
										
										
											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 = [ | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |             ['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)'] | 
					
						
							| 
									
										
										
										
											2024-12-17 14:41:23 +00:00
										 |  |  |         ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // 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]]); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-01-03 15:49:36 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // Insert known malicious networks
 | 
					
						
							|  |  |  |         $defaultBlacklist = [ | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |             ['0.0.0.0/8', true, 'Reserved address space - RFC 1122'], | 
					
						
							|  |  |  |             ['100.64.0.0/10', true, 'Carrier-grade NAT space - RFC 6598'], | 
					
						
							|  |  |  |             ['192.0.2.0/24', true, 'TEST-NET-1 Documentation space - RFC 5737'], | 
					
						
							|  |  |  |             ['198.51.100.0/24', true, 'TEST-NET-2 Documentation space - RFC 5737'], | 
					
						
							|  |  |  |             ['203.0.113.0/24', true, 'TEST-NET-3 Documentation space - RFC 5737'] | 
					
						
							| 
									
										
										
										
											2025-01-03 15:49:36 +00:00
										 |  |  |         ]; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-04 11:41:02 +00:00
										 |  |  |         $stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->blacklistTable}
 | 
					
						
							|  |  |  |             (ip_address, is_network, reason, created_by) | 
					
						
							| 
									
										
										
										
											2025-01-03 15:49:36 +00:00
										 |  |  |             VALUES (?, ?, ?, 'system')");
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         foreach ($defaultBlacklist as $ip) { | 
					
						
							|  |  |  |             $stmt->execute([$ip[0], $ip[1], $ip[2]]); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Get number of recent login attempts for an IP | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function getRecentAttempts($ip) { | 
					
						
							|  |  |  |         $stmt = $this->db->prepare("SELECT COUNT(*) as attempts FROM {$this->ratelimitTable} 
 | 
					
						
							|  |  |  |             WHERE ip_address = ? AND attempted_at > datetime('now', '-' || :minutes || ' minutes')");
 | 
					
						
							|  |  |  |         $stmt->execute([$ip, $this->decayMinutes]); | 
					
						
							|  |  |  |         $result = $stmt->fetch(PDO::FETCH_ASSOC); | 
					
						
							|  |  |  |         return intval($result['attempts']); | 
					
						
							| 
									
										
										
										
											2024-12-09 13:43:10 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Check if an IP is blacklisted | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function isIpBlacklisted($ip) { | 
					
						
							| 
									
										
										
										
											2025-01-03 15:58:19 +00:00
										 |  |  |         // First check if IP is explicitly blacklisted or in a blacklisted range
 | 
					
						
							|  |  |  |         $stmt = $this->db->prepare("SELECT ip_address, is_network, expiry_time FROM {$this->blacklistTable}"); | 
					
						
							|  |  |  |         $stmt->execute(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { | 
					
						
							|  |  |  |             // Skip expired entries
 | 
					
						
							|  |  |  |             if ($row['expiry_time'] !== null && strtotime($row['expiry_time']) < time()) { | 
					
						
							|  |  |  |                 continue; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if ($row['is_network']) { | 
					
						
							|  |  |  |                 if ($this->ipInRange($ip, $row['ip_address'])) { | 
					
						
							|  |  |  |                     return true; | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |                 if ($ip === $row['ip_address']) { | 
					
						
							|  |  |  |                     return true; | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Check if an IP is whitelisted | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function isIpWhitelisted($ip) { | 
					
						
							|  |  |  |         // 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 { | 
					
						
							| 
									
										
										
										
											2025-01-03 16:02:44 +00:00
										 |  |  |             // Check if IP is blacklisted first
 | 
					
						
							|  |  |  |             if ($this->isIpBlacklisted($ip)) { | 
					
						
							|  |  |  |                 $message = "Cannot whitelist {$ip} - IP is currently blacklisted"; | 
					
						
							|  |  |  |                 if ($userId) { | 
					
						
							|  |  |  |                     $this->log->insertLog($userId, "IP Whitelist: {$message}", 'system'); | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |                     Messages::flash('ERROR', 'DEFAULT', $message); | 
					
						
							| 
									
										
										
										
											2025-01-03 16:02:44 +00:00
										 |  |  |                 } | 
					
						
							|  |  |  |                 return false; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-04 11:41:02 +00:00
										 |  |  |             $stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->whitelistTable}
 | 
					
						
							| 
									
										
										
										
											2024-12-21 15:14:31 +00:00
										 |  |  |                 (ip_address, is_network, description, created_by) | 
					
						
							| 
									
										
										
										
											2025-01-04 11:41:02 +00:00
										 |  |  |                 VALUES (?, ?, ?, ?)");
 | 
					
						
							| 
									
										
										
										
											2024-12-21 15:14:31 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 $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'); | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |                 Messages::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage()); | 
					
						
							| 
									
										
										
										
											2024-12-21 15:14:31 +00:00
										 |  |  |             } | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-12-09 13:44:00 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-11 14:00:13 +00:00
										 |  |  |     // Remove from whitelist
 | 
					
						
							| 
									
										
										
										
											2025-01-04 11:41:02 +00:00
										 |  |  |     public function removeFromWhitelist($ip, $removedBy = 'system', $userId = null) { | 
					
						
							| 
									
										
										
										
											2024-12-22 15:21:24 +00:00
										 |  |  |         try { | 
					
						
							|  |  |  |             // Get IP details before removal for logging
 | 
					
						
							|  |  |  |             $stmt = $this->db->prepare("SELECT * FROM {$this->whitelistTable} WHERE ip_address = ?"); | 
					
						
							|  |  |  |             $stmt->execute([$ip]); | 
					
						
							|  |  |  |             $ipDetails = $stmt->fetch(PDO::FETCH_ASSOC); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // Remove the IP
 | 
					
						
							|  |  |  |             $stmt = $this->db->prepare("DELETE FROM {$this->whitelistTable} WHERE ip_address = ?"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             $result = $stmt->execute([$ip]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if ($result && $ipDetails) { | 
					
						
							|  |  |  |                 $logMessage = sprintf( | 
					
						
							|  |  |  |                     'IP Whitelist: Removed %s "%s" by %s. Was added by: %s', | 
					
						
							|  |  |  |                     $ipDetails['is_network'] ? 'network' : 'IP', | 
					
						
							|  |  |  |                     $ip, | 
					
						
							|  |  |  |                     $removedBy, | 
					
						
							|  |  |  |                     $ipDetails['created_by'] | 
					
						
							|  |  |  |                 ); | 
					
						
							|  |  |  |                 $this->log->insertLog($userId ?? 0, $logMessage, 'system'); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2024-12-09 13:44:00 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-22 15:21:24 +00:00
										 |  |  |             return $result; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         } catch (Exception $e) { | 
					
						
							|  |  |  |             if ($userId) { | 
					
						
							|  |  |  |                 $this->log->insertLog($userId, "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), 'system'); | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |                 Messages::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage()); | 
					
						
							| 
									
										
										
										
											2024-12-22 15:21:24 +00:00
										 |  |  |             } | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-12-12 14:16:48 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-03 16:06:02 +00:00
										 |  |  |     public function addToBlacklist($ip, $isNetwork = false, $reason = '', $createdBy = 'system', $userId = null, $expiryHours = null) { | 
					
						
							|  |  |  |         try { | 
					
						
							|  |  |  |             // Check if IP is whitelisted first
 | 
					
						
							|  |  |  |             if ($this->isIpWhitelisted($ip)) { | 
					
						
							|  |  |  |                 $message = "Cannot blacklist {$ip} - IP is currently whitelisted"; | 
					
						
							|  |  |  |                 if ($userId) { | 
					
						
							|  |  |  |                     $this->log->insertLog($userId, "IP Blacklist: {$message}", 'system'); | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |                     Messages::flash('ERROR', 'DEFAULT', $message); | 
					
						
							| 
									
										
										
										
											2025-01-03 16:06:02 +00:00
										 |  |  |                 } | 
					
						
							|  |  |  |                 return false; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             $expiryTime = $expiryHours ? date('Y-m-d H:i:s', strtotime("+{$expiryHours} hours")) : null; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-04 11:41:02 +00:00
										 |  |  |             $stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->blacklistTable}
 | 
					
						
							| 
									
										
										
										
											2025-01-03 16:06:02 +00:00
										 |  |  |                 (ip_address, is_network, reason, expiry_time, created_by) | 
					
						
							| 
									
										
										
										
											2025-01-04 11:41:02 +00:00
										 |  |  |                 VALUES (?, ?, ?, ?, ?)");
 | 
					
						
							| 
									
										
										
										
											2025-01-03 16:06:02 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |             $result = $stmt->execute([$ip, $isNetwork, $reason, $expiryTime, $createdBy]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if ($result) { | 
					
						
							|  |  |  |                 $logMessage = sprintf( | 
					
						
							|  |  |  |                     'IP Blacklist: Added %s "%s" by %s. Reason: %s. Expires: %s', | 
					
						
							|  |  |  |                     $isNetwork ? 'network' : 'IP', | 
					
						
							|  |  |  |                     $ip, | 
					
						
							|  |  |  |                     $createdBy, | 
					
						
							|  |  |  |                     $reason, | 
					
						
							|  |  |  |                     $expiryTime ?? 'never' | 
					
						
							|  |  |  |                 ); | 
					
						
							|  |  |  |                 $this->log->insertLog($userId ?? 0, $logMessage, 'system'); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             return $result; | 
					
						
							|  |  |  |         } catch (Exception $e) { | 
					
						
							|  |  |  |             if ($userId) { | 
					
						
							|  |  |  |                 $this->log->insertLog($userId, "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), 'system'); | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |                 Messages::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage()); | 
					
						
							| 
									
										
										
										
											2025-01-03 16:06:02 +00:00
										 |  |  |             } | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-04 11:41:02 +00:00
										 |  |  |     public function removeFromBlacklist($ip, $removedBy = 'system', $userId = null) { | 
					
						
							| 
									
										
										
										
											2025-01-03 16:06:02 +00:00
										 |  |  |         try { | 
					
						
							|  |  |  |             // Get IP details before removal for logging
 | 
					
						
							|  |  |  |             $stmt = $this->db->prepare("SELECT * FROM {$this->blacklistTable} WHERE ip_address = ?"); | 
					
						
							|  |  |  |             $stmt->execute([$ip]); | 
					
						
							|  |  |  |             $ipDetails = $stmt->fetch(PDO::FETCH_ASSOC); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // Remove the IP
 | 
					
						
							|  |  |  |             $stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable} WHERE ip_address = ?"); | 
					
						
							|  |  |  |             $result = $stmt->execute([$ip]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if ($result && $ipDetails) { | 
					
						
							|  |  |  |                 $logMessage = sprintf( | 
					
						
							|  |  |  |                     'IP Blacklist: Removed %s "%s" by %s. Was added by: %s. Reason was: %s', | 
					
						
							|  |  |  |                     $ipDetails['is_network'] ? 'network' : 'IP', | 
					
						
							|  |  |  |                     $ip, | 
					
						
							|  |  |  |                     $removedBy, | 
					
						
							|  |  |  |                     $ipDetails['created_by'], | 
					
						
							|  |  |  |                     $ipDetails['reason'] | 
					
						
							|  |  |  |                 ); | 
					
						
							|  |  |  |                 $this->log->insertLog($userId ?? 0, $logMessage, 'system'); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             return $result; | 
					
						
							|  |  |  |         } catch (Exception $e) { | 
					
						
							|  |  |  |             if ($userId) { | 
					
						
							|  |  |  |                 $this->log->insertLog($userId, "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), 'system'); | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |                 Messages::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage()); | 
					
						
							| 
									
										
										
										
											2025-01-03 16:06:02 +00:00
										 |  |  |             } | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-12 14:16:48 +00:00
										 |  |  |     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
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-03 16:07:34 +00:00
										 |  |  |     public function getBlacklistedIps() { | 
					
						
							|  |  |  |         $stmt = $this->db->prepare("SELECT * FROM {$this->blacklistTable} ORDER BY created_at DESC"); | 
					
						
							|  |  |  |         $stmt->execute(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $stmt->fetchAll(PDO::FETCH_ASSOC); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-03 16:08:59 +00:00
										 |  |  |     public function cleanupExpiredEntries() { | 
					
						
							|  |  |  |         try { | 
					
						
							|  |  |  |             // Remove expired blacklist entries
 | 
					
						
							|  |  |  |             $stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable}
 | 
					
						
							| 
									
										
										
										
											2025-01-04 09:46:07 +00:00
										 |  |  |                 WHERE expiry_time IS NOT NULL AND expiry_time < datetime('now')");
 | 
					
						
							| 
									
										
										
										
											2025-01-03 16:08:59 +00:00
										 |  |  |             $stmt->execute(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // Clean old login attempts
 | 
					
						
							| 
									
										
										
										
											2025-01-04 09:32:19 +00:00
										 |  |  |             $stmt = $this->db->prepare("DELETE FROM {$this->ratelimitTable}
 | 
					
						
							| 
									
										
										
										
											2025-01-04 09:46:07 +00:00
										 |  |  |                 WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')");
 | 
					
						
							| 
									
										
										
										
											2025-01-04 09:32:19 +00:00
										 |  |  |             $stmt->execute([':minutes' => $this->decayMinutes]); | 
					
						
							| 
									
										
										
										
											2025-01-03 16:08:59 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |             return true; | 
					
						
							|  |  |  |         } catch (Exception $e) { | 
					
						
							|  |  |  |             $this->log->insertLog(0, "Failed to cleanup expired entries: " . $e->getMessage(), 'system'); | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |             Messages::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage()); | 
					
						
							| 
									
										
										
										
											2025-01-03 16:08:59 +00:00
										 |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-04 09:32:19 +00:00
										 |  |  |     public function isAllowed($username, $ipAddress) { | 
					
						
							|  |  |  |         // First check if IP is blacklisted
 | 
					
						
							|  |  |  |         if ($this->isIpBlacklisted($ipAddress)) { | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Then check if IP is whitelisted
 | 
					
						
							| 
									
										
										
										
											2024-12-10 13:56:18 +00:00
										 |  |  |         if ($this->isIpWhitelisted($ipAddress)) { | 
					
						
							|  |  |  |             return true; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |         // Clean old attempts
 | 
					
						
							|  |  |  |         $this->clearOldAttempts(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-04 09:32:19 +00:00
										 |  |  |         // Check if we've hit the rate limit
 | 
					
						
							|  |  |  |         if ($this->tooManyAttempts($username, $ipAddress)) { | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Check total attempts across all usernames from this IP
 | 
					
						
							|  |  |  |         $sql = "SELECT COUNT(*) as total_attempts
 | 
					
						
							|  |  |  |                 FROM {$this->ratelimitTable} | 
					
						
							|  |  |  |                 WHERE ip_address = :ip | 
					
						
							| 
									
										
										
										
											2025-01-04 09:46:07 +00:00
										 |  |  |                 AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
 | 
					
						
							| 
									
										
										
										
											2025-01-04 09:32:19 +00:00
										 |  |  |         $stmt = $this->db->prepare($sql); | 
					
						
							|  |  |  |         $stmt->execute([ | 
					
						
							|  |  |  |             ':ip'      => $ipAddress, | 
					
						
							|  |  |  |             ':minutes' => $this->decayMinutes | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  |         $result = $stmt->fetch(PDO::FETCH_ASSOC); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Check if we would hit auto-blacklist threshold
 | 
					
						
							|  |  |  |         return $result['total_attempts'] < $this->autoBlacklistThreshold; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function attempt($username, $ipAddress) { | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |         // 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([ | 
					
						
							| 
									
										
										
										
											2025-01-04 09:32:19 +00:00
										 |  |  |             ':ip'           => $ipAddress, | 
					
						
							|  |  |  |             ':username'     => $username | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-04 09:32:19 +00:00
										 |  |  |         // Auto-blacklist if too many attempts
 | 
					
						
							|  |  |  |         if (!$this->isAllowed($username, $ipAddress)) { | 
					
						
							|  |  |  |             $this->addToBlacklist( | 
					
						
							|  |  |  |                 $ipAddress, | 
					
						
							|  |  |  |                 false, | 
					
						
							|  |  |  |                 'Auto-blacklisted due to excessive login attempts', | 
					
						
							|  |  |  |                 'system', | 
					
						
							|  |  |  |                 null, | 
					
						
							|  |  |  |                 $this->autoBlacklistDuration | 
					
						
							|  |  |  |             ); | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return true; | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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 | 
					
						
							| 
									
										
										
										
											2025-01-04 09:46:07 +00:00
										 |  |  |                 AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
 | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |         $stmt = $this->db->prepare($sql); | 
					
						
							|  |  |  |         $stmt->execute([ | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |             ':ip'           => $ipAddress, | 
					
						
							|  |  |  |             ':username'     => $username, | 
					
						
							|  |  |  |             ':minutes'      => $this->decayMinutes | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $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}
 | 
					
						
							| 
									
										
										
										
											2025-01-04 09:46:07 +00:00
										 |  |  |                 WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')";
 | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |         $stmt = $this->db->prepare($sql); | 
					
						
							|  |  |  |         $stmt->execute([ | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |             ':minutes'      => $this->decayMinutes | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |         ]); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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 | 
					
						
							| 
									
										
										
										
											2025-01-04 09:46:07 +00:00
										 |  |  |                 AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
 | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |         $stmt = $this->db->prepare($sql); | 
					
						
							|  |  |  |         $stmt->execute([ | 
					
						
							| 
									
										
										
										
											2025-01-06 09:13:28 +00:00
										 |  |  |             ':ip'           => $ipAddress, | 
					
						
							|  |  |  |             ':username'     => $username, | 
					
						
							|  |  |  |             ':minutes'      => $this->decayMinutes | 
					
						
							| 
									
										
										
										
											2024-12-06 13:25:15 +00:00
										 |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $result = $stmt->fetch(PDO::FETCH_ASSOC); | 
					
						
							|  |  |  |         return max(0, $this->maxAttempts - $result['attempts']); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function getDecayMinutes() { | 
					
						
							|  |  |  |         return $this->decayMinutes; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |