PHP WebShell
Текущая директория: /var/www/bitcardoApp/lib
Просмотр файла: rate_limit.php
<?php
/**
* lib/rate_limit.php
* Simple DB-backed rate limiter with per-bucket tokens.
*
* Buckets you might use:
* - 'ip' token = $_SERVER['REMOTE_ADDR']
* - 'login' token = rl_norm_login($login)
* - 'otp_send' token = 'user_' . $userId
* - 'otp_verify' token = 'user_' . $userId
* - 'totp_verify'token = 'user_' . $userId
*
* TABLE (if not created yet):
* CREATE TABLE IF NOT EXISTS rate_limits (
* bucket VARCHAR(32) NOT NULL,
* token VARCHAR(190) NOT NULL,
* counter INT UNSIGNED NOT NULL DEFAULT 0,
* window_start DATETIME NOT NULL,
* locked_until DATETIME DEFAULT NULL,
* last_hit DATETIME NOT NULL,
* PRIMARY KEY (bucket, token),
* KEY idx_locked (bucket, locked_until)
* ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/
/* ---------- Safe lowercase (no mbstring required) ---------- */
if (!function_exists('rl_mb_lower')) {
function rl_mb_lower(string $s): string {
return function_exists('mb_strtolower') ? mb_strtolower($s, 'UTF-8') : strtolower($s);
}
}
/* ---------- Normalize login identifier (email/phone) ---------- */
if (!function_exists('rl_norm_login')) {
function rl_norm_login(string $login): string {
$login = trim($login);
// If it looks like an email, lowercase + trim spaces
if (strpos($login, '@') !== false) {
// strip surrounding spaces and lowercase
$login = rl_mb_lower($login);
return substr($login, 0, 190);
}
// Otherwise treat as phone: keep + and digits only, collapse leading zeros
$phone = preg_replace('/[^\d+]+/', '', $login);
return substr($phone, 0, 64);
}
}
/**
* Check & increment a limiter.
*
* @param mysqli $conn
* @param string $bucket e.g. 'ip', 'login', 'otp_send', 'otp_verify'
* @param string $token caller-provided token (NEVER read $userId here)
* @param int $limit max allowed within window
* @param int $windowSec rolling window size in seconds
* @param int $lockSec lockout duration (seconds) after exceeding limit
* @return array { ok: bool, locked_for: int }
*/
if (!function_exists('rl_check_and_inc')) {
function rl_check_and_inc(mysqli $conn, string $bucket, string $token, int $limit, int $windowSec, int $lockSec): array {
$now = new DateTimeImmutable();
$nowSql = $now->format('Y-m-d H:i:s');
$stmt = $conn->prepare("SELECT counter, window_start, locked_until FROM rate_limits WHERE bucket=? AND token=? LIMIT 1");
$stmt->bind_param('ss', $bucket, $token);
$stmt->execute();
$stmt->bind_result($counter, $windowStart, $lockedUntil);
$has = $stmt->fetch();
$stmt->close();
// If locked, return remaining seconds
if ($has && $lockedUntil) {
$lu = new DateTimeImmutable($lockedUntil);
if ($lu > $now) {
return ['ok' => false, 'locked_for' => max(1, $lu->getTimestamp() - $now->getTimestamp())];
}
}
// Calculate if within window
$reset = true;
if ($has && $windowStart) {
$ws = new DateTimeImmutable($windowStart);
$reset = (($now->getTimestamp() - $ws->getTimestamp()) >= $windowSec);
}
if (!$has) {
// Insert new row
$counterNew = 1;
$wsSql = $nowSql;
$stmt = $conn->prepare("INSERT INTO rate_limits (bucket, token, counter, window_start, locked_until, last_hit)
VALUES (?, ?, ?, ?, NULL, ?)");
$stmt->bind_param('ssiss', $bucket, $token, $counterNew, $wsSql, $nowSql);
$stmt->execute();
$stmt->close();
// within limit
return ['ok' => true, 'locked_for' => 0];
}
if ($reset) {
// New window
$counterNew = 1;
$wsSql = $nowSql;
$stmt = $conn->prepare("UPDATE rate_limits SET counter=?, window_start=?, locked_until=NULL, last_hit=? WHERE bucket=? AND token=?");
$stmt->bind_param('issss', $counterNew, $wsSql, $nowSql, $bucket, $token);
$stmt->execute();
$stmt->close();
return ['ok' => true, 'locked_for' => 0];
}
// Same window → increment
$counter++;
if ($counter > $limit) {
// lock
$luSql = $now->modify("+{$lockSec} seconds")->format('Y-m-d H:i:s');
$stmt = $conn->prepare("UPDATE rate_limits SET counter=?, locked_until=?, last_hit=? WHERE bucket=? AND token=?");
$stmt->bind_param('issss', $counter, $luSql, $nowSql, $bucket, $token);
$stmt->execute();
$stmt->close();
return ['ok' => false, 'locked_for' => $lockSec];
} else {
$stmt = $conn->prepare("UPDATE rate_limits SET counter=?, last_hit=? WHERE bucket=? AND token=?");
$stmt->bind_param('isss', $counter, $nowSql, $bucket, $token);
$stmt->execute();
$stmt->close();
return ['ok' => true, 'locked_for' => 0];
}
}
}
/**
* Reset a limiter (e.g., after successful login/verification).
*/
if (!function_exists('rl_reset')) {
function rl_reset(mysqli $conn, string $bucket, string $token): void {
$stmt = $conn->prepare("DELETE FROM rate_limits WHERE bucket=? AND token=?");
$stmt->bind_param('ss', $bucket, $token);
$stmt->execute();
$stmt->close();
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!