128 lines
3.6 KiB
PHP
128 lines
3.6 KiB
PHP
|
|
<?php
|
||
|
|
/**
|
||
|
|
* Rate Limiting Middleware
|
||
|
|
*
|
||
|
|
* Implements session-based rate limiting to prevent abuse.
|
||
|
|
*/
|
||
|
|
class RateLimitMiddleware {
|
||
|
|
// Default limits
|
||
|
|
const DEFAULT_LIMIT = 100; // requests per window
|
||
|
|
const API_LIMIT = 60; // API requests per window
|
||
|
|
const WINDOW_SECONDS = 60; // 1 minute window
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check rate limit for current request
|
||
|
|
*
|
||
|
|
* @param string $type 'default' or 'api'
|
||
|
|
* @return bool True if request is allowed, false if rate limited
|
||
|
|
*/
|
||
|
|
public static function check($type = 'default') {
|
||
|
|
if (session_status() === PHP_SESSION_NONE) {
|
||
|
|
session_start();
|
||
|
|
}
|
||
|
|
|
||
|
|
$limit = $type === 'api' ? self::API_LIMIT : self::DEFAULT_LIMIT;
|
||
|
|
$key = 'rate_limit_' . $type;
|
||
|
|
$now = time();
|
||
|
|
|
||
|
|
// Initialize rate limit tracking
|
||
|
|
if (!isset($_SESSION[$key])) {
|
||
|
|
$_SESSION[$key] = [
|
||
|
|
'count' => 0,
|
||
|
|
'window_start' => $now
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$rateData = &$_SESSION[$key];
|
||
|
|
|
||
|
|
// Check if window has expired
|
||
|
|
if ($now - $rateData['window_start'] >= self::WINDOW_SECONDS) {
|
||
|
|
// Reset for new window
|
||
|
|
$rateData['count'] = 0;
|
||
|
|
$rateData['window_start'] = $now;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Increment request count
|
||
|
|
$rateData['count']++;
|
||
|
|
|
||
|
|
// Check if over limit
|
||
|
|
if ($rateData['count'] > $limit) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Apply rate limiting and send error response if exceeded
|
||
|
|
*
|
||
|
|
* @param string $type 'default' or 'api'
|
||
|
|
*/
|
||
|
|
public static function apply($type = 'default') {
|
||
|
|
if (!self::check($type)) {
|
||
|
|
http_response_code(429);
|
||
|
|
header('Content-Type: application/json');
|
||
|
|
header('Retry-After: ' . self::WINDOW_SECONDS);
|
||
|
|
echo json_encode([
|
||
|
|
'success' => false,
|
||
|
|
'error' => 'Rate limit exceeded. Please try again later.',
|
||
|
|
'retry_after' => self::WINDOW_SECONDS
|
||
|
|
]);
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get current rate limit status
|
||
|
|
*
|
||
|
|
* @param string $type 'default' or 'api'
|
||
|
|
* @return array Rate limit status
|
||
|
|
*/
|
||
|
|
public static function getStatus($type = 'default') {
|
||
|
|
if (session_status() === PHP_SESSION_NONE) {
|
||
|
|
session_start();
|
||
|
|
}
|
||
|
|
|
||
|
|
$limit = $type === 'api' ? self::API_LIMIT : self::DEFAULT_LIMIT;
|
||
|
|
$key = 'rate_limit_' . $type;
|
||
|
|
$now = time();
|
||
|
|
|
||
|
|
if (!isset($_SESSION[$key])) {
|
||
|
|
return [
|
||
|
|
'limit' => $limit,
|
||
|
|
'remaining' => $limit,
|
||
|
|
'reset' => $now + self::WINDOW_SECONDS
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$rateData = $_SESSION[$key];
|
||
|
|
|
||
|
|
// Check if window has expired
|
||
|
|
if ($now - $rateData['window_start'] >= self::WINDOW_SECONDS) {
|
||
|
|
return [
|
||
|
|
'limit' => $limit,
|
||
|
|
'remaining' => $limit,
|
||
|
|
'reset' => $now + self::WINDOW_SECONDS
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
return [
|
||
|
|
'limit' => $limit,
|
||
|
|
'remaining' => max(0, $limit - $rateData['count']),
|
||
|
|
'reset' => $rateData['window_start'] + self::WINDOW_SECONDS
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add rate limit headers to response
|
||
|
|
*
|
||
|
|
* @param string $type 'default' or 'api'
|
||
|
|
*/
|
||
|
|
public static function addHeaders($type = 'default') {
|
||
|
|
$status = self::getStatus($type);
|
||
|
|
header('X-RateLimit-Limit: ' . $status['limit']);
|
||
|
|
header('X-RateLimit-Remaining: ' . $status['remaining']);
|
||
|
|
header('X-RateLimit-Reset: ' . $status['reset']);
|
||
|
|
}
|
||
|
|
}
|