Compare commits
4 Commits
645e98cd6a
...
bc1089be21
Author | SHA1 | Date |
---|---|---|
|
bc1089be21 | |
|
a0747cfbc8 | |
|
0f72f3bea4 | |
|
38e4b002c8 |
|
@ -4,30 +4,45 @@ class RateLimiter {
|
||||||
private $db;
|
private $db;
|
||||||
private $maxAttempts = 5; // Maximum login attempts
|
private $maxAttempts = 5; // Maximum login attempts
|
||||||
private $decayMinutes = 15; // Time window in minutes
|
private $decayMinutes = 15; // Time window in minutes
|
||||||
private $tableName = 'login_attempts';
|
private $ratelimitTable = 'login_attempts';
|
||||||
|
private $whitelistTable = 'ip_whitelist';
|
||||||
private $whitelistedIps = []; // Whitelisted IPs
|
private $whitelistedIps = []; // Whitelisted IPs
|
||||||
private $whitelistedNetworks = []; // Whitelisted CIDR ranges
|
private $whitelistedNetworks = []; // Whitelisted CIDR ranges
|
||||||
|
|
||||||
public function __construct($database) {
|
public function __construct($database) {
|
||||||
$this->db = $database->getConnection();
|
$this->db = $database->getConnection();
|
||||||
$this->createTableIfNotExists();
|
$this->createTablesIfNotExists();
|
||||||
$this->loadWhitelist();
|
$this->loadWhitelist();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createTableIfNotExists() {
|
// Database preparation
|
||||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->tableName} (
|
private function createTablesIfNotExists() {
|
||||||
|
// Login attempts table
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS {$this->ratelimitTable} (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
ip_address VARCHAR(45) NOT NULL,
|
||||||
username VARCHAR(255) NOT NULL,
|
username VARCHAR(255) NOT NULL,
|
||||||
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
INDEX idx_ip_username (ip_address, username)
|
INDEX idx_ip_username (ip_address, username)
|
||||||
)";
|
)";
|
||||||
|
$this->db->exec($sql);
|
||||||
|
|
||||||
|
// IP whitelist table
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
|
||||||
|
id int(11) PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
ip_address VARCHAR(45) NOT NULL,
|
||||||
|
is_network BOOLEAN DEFAULT FALSE,
|
||||||
|
description VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_by VARCHAR(255),
|
||||||
|
UNIQUE KEY unique_ip (ip_address)
|
||||||
|
)";
|
||||||
$this->db->exec($sql);
|
$this->db->exec($sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List of IPs to bypass rate limiting
|
||||||
private function loadWhitelist() {
|
private function loadWhitelist() {
|
||||||
// Load from database or config
|
// FIXME Load from database or config
|
||||||
$this->whitelistedIps = [
|
$this->whitelistedIps = [
|
||||||
'127.0.0.1', // localhost
|
'127.0.0.1', // localhost
|
||||||
'::1' // localhost IPv6
|
'::1' // localhost IPv6
|
||||||
|
@ -40,20 +55,25 @@ class RateLimiter {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if IP is whitelisted
|
||||||
private function isIpWhitelisted($ip) {
|
private function isIpWhitelisted($ip) {
|
||||||
// Check exact IP match
|
// Check exact IP match and CIDR ranges
|
||||||
if (in_array($ip, $this->whitelistedIps)) {
|
$stmt = $this->db->prepare("SELECT ip_address, is_network FROM {$this->whitelistTable}");
|
||||||
return true;
|
$stmt->execute();
|
||||||
}
|
|
||||||
|
|
||||||
// Check CIDR ranges
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
foreach ($this->whitelistedNetworks as $network) {
|
if ($row['is_network']) {
|
||||||
if ($this->ipInRange($ip, $network)) {
|
if ($this->ipInRange($ip, $row['ip_address'])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
if ($ip === $row['ip_address']) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ipInRange($ip, $cidr) {
|
private function ipInRange($ip, $cidr) {
|
||||||
|
@ -67,28 +87,31 @@ class RateLimiter {
|
||||||
return ($ip & $mask) == $subnet;
|
return ($ip & $mask) == $subnet;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addToWhitelist($ip, $isNetwork = false) {
|
// Add to whitelist
|
||||||
if ($isNetwork) {
|
public function addToWhitelist($ip, $isNetwork = false, $description = '', $createdBy = 'system') {
|
||||||
if (!in_array($ip, $this->whitelistedNetworks)) {
|
$stmt = $this->db->prepare("INSERT INTO {$this->whitelistTable}
|
||||||
$this->whitelistedNetworks[] = $ip;
|
(ip_address, is_network, description, created_by)
|
||||||
}
|
VALUES (?, ?, ?, ?)
|
||||||
} else {
|
ON DUPLICATE KEY UPDATE
|
||||||
if (!in_array($ip, $this->whitelistedIps)) {
|
is_network = VALUES(is_network),
|
||||||
$this->whitelistedIps[] = $ip;
|
description = VALUES(description),
|
||||||
}
|
created_by = VALUES(created_by)");
|
||||||
}
|
|
||||||
|
return $stmt->execute([$ip, $isNetwork, $description, $createdBy]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove from whitelist
|
||||||
public function removeFromWhitelist($ip) {
|
public function removeFromWhitelist($ip) {
|
||||||
$indexIp = array_search($ip, $this->whitelistedIps);
|
$stmt = $this->db->prepare("DELETE FROM {$this->whitelistTable} WHERE ip_address = ?");
|
||||||
if ($indexIp !== false) {
|
|
||||||
unset($this->whitelistedIps[$indexIp]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$indexNetwork = array_search($ip, $this->whitelistedNetworks);
|
return $stmt->execute([$ip]);
|
||||||
if ($indexNetwork !== false) {
|
}
|
||||||
unset($this->whitelistedNetworks[$indexNetwork]);
|
|
||||||
}
|
public function getWhitelistedIps() {
|
||||||
|
$stmt = $this->db->prepare("SELECT * FROM {$this->whitelistTable} ORDER BY created_at DESC");
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function attempt($username, $ipAddress) {
|
public function attempt($username, $ipAddress) {
|
||||||
|
@ -101,7 +124,7 @@ class RateLimiter {
|
||||||
$this->clearOldAttempts();
|
$this->clearOldAttempts();
|
||||||
|
|
||||||
// Record this attempt
|
// Record this attempt
|
||||||
$sql = "INSERT INTO {$this->tableName} (ip_address, username) VALUES (:ip, :username)";
|
$sql = "INSERT INTO {$this->ratelimitTable} (ip_address, username) VALUES (:ip, :username)";
|
||||||
$stmt = $this->db->prepare($sql);
|
$stmt = $this->db->prepare($sql);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
':ip' => $ipAddress,
|
':ip' => $ipAddress,
|
||||||
|
@ -114,7 +137,7 @@ class RateLimiter {
|
||||||
|
|
||||||
public function tooManyAttempts($username, $ipAddress) {
|
public function tooManyAttempts($username, $ipAddress) {
|
||||||
$sql = "SELECT COUNT(*) as attempts
|
$sql = "SELECT COUNT(*) as attempts
|
||||||
FROM {$this->tableName}
|
FROM {$this->ratelimitTable}
|
||||||
WHERE ip_address = :ip
|
WHERE ip_address = :ip
|
||||||
AND username = :username
|
AND username = :username
|
||||||
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
|
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
|
||||||
|
@ -131,7 +154,7 @@ class RateLimiter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function clearOldAttempts() {
|
public function clearOldAttempts() {
|
||||||
$sql = "DELETE FROM {$this->tableName}
|
$sql = "DELETE FROM {$this->ratelimitTable}
|
||||||
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')";
|
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')";
|
||||||
|
|
||||||
$stmt = $this->db->prepare($sql);
|
$stmt = $this->db->prepare($sql);
|
||||||
|
@ -142,7 +165,7 @@ class RateLimiter {
|
||||||
|
|
||||||
public function getRemainingAttempts($username, $ipAddress) {
|
public function getRemainingAttempts($username, $ipAddress) {
|
||||||
$sql = "SELECT COUNT(*) as attempts
|
$sql = "SELECT COUNT(*) as attempts
|
||||||
FROM {$this->tableName}
|
FROM {$this->ratelimitTable}
|
||||||
WHERE ip_address = :ip
|
WHERE ip_address = :ip
|
||||||
AND username = :username
|
AND username = :username
|
||||||
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
|
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
|
||||||
|
|
Loading…
Reference in New Issue