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();
  }
}

Выполнить команду


Для локальной разработки. Не используйте в интернете!